diff --git a/.github/workflows/n_updater.yml b/.github/workflows/n_updater.yml index ce3e9c925..a1a835a78 100644 --- a/.github/workflows/n_updater.yml +++ b/.github/workflows/n_updater.yml @@ -20,9 +20,11 @@ jobs: # Setting up Git user git config --global user.name 'yunohost-bot' git config --global user.email 'yunohost-bot@users.noreply.github.com' - # Run the updater script + # Download n wget https://raw.githubusercontent.com/tj/n/master/bin/n --output-document=helpers/vendor/n/n + # Proceed only if there is a change [[ -z "$(git diff helpers/vendor/n/n)" ]] || echo "PROCEED=true" >> $GITHUB_ENV + echo "VERSION=$(sed -n 's/^VERSION=\"\(.*\)\"/\1/p' < n)" >> $GITHUB_ENV - name: Commit changes id: commit if: ${{ env.PROCEED == 'true' }} @@ -34,14 +36,14 @@ jobs: uses: peter-evans/create-pull-request@v3 with: token: ${{ secrets.GITHUB_TOKEN }} - commit-message: Update n to version ${{ env.VERSION }} + commit-message: Update n to ${{ env.VERSION }} committer: 'yunohost-bot ' author: 'yunohost-bot ' signoff: false base: dev branch: ci-auto-update-n-v${{ env.VERSION }} delete-branch: true - title: 'Upgrade n to version ${{ env.VERSION }}' + title: 'Upgrade n to ${{ env.VERSION }}' body: | - Upgrade `n` to v${{ env.VERSION }} + Upgrade `n` to ${{ env.VERSION }} draft: false diff --git a/conf/dovecot/dovecot.conf b/conf/dovecot/dovecot.conf index be24bfaf2..858777a91 100644 --- a/conf/dovecot/dovecot.conf +++ b/conf/dovecot/dovecot.conf @@ -118,8 +118,8 @@ plugin { antispam_debug_target = syslog antispam_verbose_debug = 0 antispam_backend = pipe - antispam_spam = Junk;SPAM - antispam_trash = Trash + antispam_spam_pattern_ignorecase = junk;spam + antispam_trash_pattern_ignorecase = trash;papierkorb;deleted messages antispam_pipe_program = /usr/bin/rspamc antispam_pipe_program_args = -h;localhost:11334;-P;q1 antispam_pipe_program_spam_arg = learn_spam diff --git a/conf/rspamd/redis.conf b/conf/rspamd/redis.conf new file mode 100644 index 000000000..03152581e --- /dev/null +++ b/conf/rspamd/redis.conf @@ -0,0 +1,2 @@ +# set redis server +servers = "127.0.0.1"; diff --git a/debian/changelog b/debian/changelog index fda65e328..508091e94 100644 --- a/debian/changelog +++ b/debian/changelog @@ -4,6 +4,24 @@ yunohost (12.0.0) unstable; urgency=low -- Alexandre Aubin Thu, 04 May 2023 20:30:19 +0200 +yunohost (11.2.5) stable; urgency=low + + - debian: fix conflict with openssl that is too harsh, openssl version on bullseye is now 1.1.1w, bookworm has 3.x (e8700bfe7) + - dyndns: tweak dyndns subscribe/unsubscribe for dyndns recovery password integration in webadmin ([#1715](https://github.com/yunohost/yunohost/pull/1715)) + - helpers: ynh_setup_source: check and re-download a prefetched file that doesn't match the checksum (3dfab89c1) + - helpers: ynh_setup_source: fix misleading example ([#1714](https://github.com/yunohost/yunohost/pull/1714)) + - helpers: php/apt: allow `phpX.Y` as sole dependency for `$phpversion=X.Y` ([#1722](https://github.com/yunohost/yunohost/pull/1722)) + - apps: fix typo in log statement ([#1709](https://github.com/yunohost/yunohost/pull/1709)) + - apps: allow system users to send mails from IPv6 localhost. ([#1710](https://github.com/yunohost/yunohost/pull/1710)) + - apps: add "support_purge" to app info for webadmin integration ([#1719](https://github.com/yunohost/yunohost/pull/1719)) + - diagnosis: be more flexible regarding accepted values for DMARC DNS records ([#1713](https://github.com/yunohost/yunohost/pull/1713)) + - dns: add home.arpa as special TLD (#1718) (bb097fedc) + - i18n: Translations updated for Basque, French + + Thanks to all contributors <3 ! (axolotle, Florian, Kayou, orhtej2, Pierre de La Morinerie, ppr, stanislas, tituspijean, xabirequejo) + + -- Alexandre Aubin Mon, 09 Oct 2023 23:16:13 +0200 + yunohost (11.2.4) stable; urgency=low - doc: Improve --help for 'yunohost app install' ([#1702](https://github.com/yunohost/yunohost/pull/1702)) diff --git a/helpers/apt b/helpers/apt index a2f2d3de8..8ff9a3cd1 100644 --- a/helpers/apt +++ b/helpers/apt @@ -250,7 +250,7 @@ ynh_install_app_dependencies() { # Check for specific php dependencies which requires sury # This grep will for example return "7.4" if dependencies is "foo bar php7.4-pwet php-gni" # The (?<=php) syntax corresponds to lookbehind ;) - local specific_php_version=$(echo $dependencies | grep -oP '(?<=php)[0-9.]+(?=-|\>)' | sort -u) + local specific_php_version=$(echo $dependencies | grep -oP '(?<=php)[0-9.]+(?=-|\>|)' | sort -u) if [[ -n "$specific_php_version" ]] then diff --git a/helpers/vendor/n/n b/helpers/vendor/n/n index 2a877c45b..eb276a2f5 100755 --- a/helpers/vendor/n/n +++ b/helpers/vendor/n/n @@ -61,7 +61,7 @@ function n_grep() { # Setup and state # -VERSION="v9.1.0" +VERSION="v9.2.0" N_PREFIX="${N_PREFIX-/usr/local}" N_PREFIX=${N_PREFIX%/} @@ -135,6 +135,7 @@ g_target_node= DOWNLOAD=false # set to opt-out of activate (install), and opt-in to download (run, exec) ARCH= SHOW_VERBOSE_LOG="true" +OFFLINE=false # ANSI escape codes # https://en.wikipedia.org/wiki/ANSI_escape_code @@ -393,6 +394,7 @@ Options: -q, --quiet Disable curl output. Disable log messages processing "auto" and "engine" labels. -d, --download Download if necessary, and don't make active -a, --arch Override system architecture + --offline Resolve target version against cached downloads instead of internet lookup --all ls-remote displays all matches instead of last 20 --insecure Turn off certificate checking for https requests (may be needed from behind a proxy server) --use-xz/--no-use-xz Override automatic detection of xz support and enable/disable use of xz compressed node downloads. @@ -784,6 +786,9 @@ install() { exit fi fi + if [[ "$OFFLINE" == "true" ]]; then + abort "version unavailable offline" + fi log installing "${g_mirror_folder_name}-v$version" @@ -1103,6 +1108,7 @@ function get_package_engine_version() { verbose_log "target" "${version}" else command -v npx &> /dev/null || abort "an active version of npx is required to use complex 'engine' ranges from package.json" + [[ "$OFFLINE" != "true" ]] || abort "offline: an internet connection is required for looking up complex 'engine' ranges from package.json" verbose_log "resolving" "${range}" local version_per_line="$(n lsr --all)" local versions_one_line=$(echo "${version_per_line}" | tr '\n' ' ') @@ -1199,6 +1205,8 @@ function get_latest_resolved_version() { # Just numbers, already resolved, no need to lookup first. simple_version="${simple_version#v}" g_target_node="${simple_version}" + elif [[ "$OFFLINE" == "true" ]]; then + g_target_node=$(display_local_versions "${version}") else # Complicated recognising exact version, KISS and lookup. g_target_node=$(N_MAX_REMOTE_MATCHES=1 display_remote_versions "$version") @@ -1232,6 +1240,56 @@ function display_match_limit(){ fi } +# +# Synopsis: display_local_versions version +# + +function display_local_versions() { + local version="$1" + local match='.' + verbose_log "offline" "matching cached versions" + + # Transform some labels before processing further. + if is_node_support_version "${version}"; then + version="$(display_latest_node_support_alias "${version}")" + match_count=1 + elif [[ "${version}" = "auto" ]]; then + # suppress stdout logging so lsr layout same as usual for scripting + get_auto_version || return 2 + version="${g_target_node}" + elif [[ "${version}" = "engine" ]]; then + # suppress stdout logging so lsr layout same as usual for scripting + get_engine_version || return 2 + version="${g_target_node}" + fi + + if [[ "${version}" = "latest" || "${version}" = "current" ]]; then + match='^node/.' + elif is_exact_numeric_version "${version}"; then + # Quote any dots in version so they are literal for expression + match="^node/${version//\./\.}" + elif is_numeric_version "${version}"; then + version="${version#v}" + # Quote any dots in version so they are literal for expression + match="${version//\./\.}" + # Avoid 1.2 matching 1.23 + match="^node/${match}[^0-9]" + # elif is_lts_codename "${version}"; then + # see if demand + elif is_download_folder "${version}"; then + match="^${version}/" + # elif is_download_version "${version}"; then + # see if demand + else + abort "invalid version '$1' for offline matching" + fi + + display_versions_paths \ + | n_grep -E "${match}" \ + | tail -n 1 \ + | sed 's|node/||' +} + # # Synopsis: display_remote_versions version # @@ -1577,6 +1635,7 @@ while [[ $# -ne 0 ]]; do -h|--help|help) display_help; exit ;; -q|--quiet) set_quiet ;; -d|--download) DOWNLOAD="true" ;; + --offline) OFFLINE="true" ;; --insecure) set_insecure ;; -p|--preserve) N_PRESERVE_NPM="true" N_PRESERVE_COREPACK="true" ;; --no-preserve) N_PRESERVE_NPM="" N_PRESERVE_COREPACK="" ;; diff --git a/hooks/conf_regen/31-rspamd b/hooks/conf_regen/31-rspamd index 6807ce0cd..6333a755f 100755 --- a/hooks/conf_regen/31-rspamd +++ b/hooks/conf_regen/31-rspamd @@ -13,6 +13,8 @@ do_pre_regen() { "${pending_dir}/etc/rspamd/local.d/dkim_signing.conf" install -D -m 644 rspamd.sieve \ "${pending_dir}/etc/dovecot/global_script/rspamd.sieve" + install -D -m 644 redis.conf \ + "${pending_dir}/etc/rspamd/local.d/redis.conf" } do_post_regen() { diff --git a/locales/en.json b/locales/en.json index 318bee727..64a5e6bf3 100644 --- a/locales/en.json +++ b/locales/en.json @@ -91,6 +91,7 @@ "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_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_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", @@ -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_subscribed": "DynDNS domain subscribed", "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_unsubscribed": "DynDNS domain unsubscribed", "dyndns_unsubscribe_denied": "Failed to unsubscribe domain: invalid credentials", diff --git a/locales/eu.json b/locales/eu.json index 0267b3366..85e7c36fc 100644 --- a/locales/eu.json +++ b/locales/eu.json @@ -766,5 +766,13 @@ "group_mailalias_add": "'{mail}' ePosta aliasa jarri zaio '{group}' taldeari", "group_mailalias_remove": "'{mail}' ePosta aliasa kendu zaio '{group}' taldeari", "group_user_remove": "'{user}' erabiltzailea '{group}' taldetik kenduko da", - "group_user_add": "'{user}' erabiltzailea '{group}' taldera gehituko da" + "group_user_add": "'{user}' erabiltzailea '{group}' taldera gehituko da", + "ask_dyndns_recovery_password_explain": "Aukeratu DynDNS domeinua berreskuratzeko pasahitza, etorkizunean berrezarri beharko bazenu.", + "ask_dyndns_recovery_password_explain_during_unsubscribe": "Sartu DynDNS domeinurako berreskuraketa pasahitza.", + "dyndns_no_recovery_password": "Ez da berreskurapen pasahitzik zehaztu! Domeinuaren gaineko kontrola galduz gero, YunoHost taldeko administrariarekin jarri beharko zara harremanetan!", + "ask_dyndns_recovery_password": "DynDNS berreskuratzeko pasahitza", + "dyndns_subscribed": "DynDNS domeinua harpidetu da", + "dyndns_subscribe_failed": "Ezin izan da DynDNS domeinua harpidetu: {error}", + "dyndns_unsubscribe_failed": "Ezin izan da DynDNS domeinuaren harpidetza utzi: {error}", + "dyndns_unsubscribed": "DynDNS domeinuaren harpidetza utzi da" } diff --git a/locales/fr.json b/locales/fr.json index 2cc0f612c..2e4d1602b 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -781,6 +781,8 @@ "dyndns_set_recovery_password_unknown_domain": "Échec de la définition du mot de passe de récupération : le domaine n'est pas enregistré", "dyndns_set_recovery_password_invalid_password": "Échec de la définition du mot de passe de récupération : le mot de passe n'est pas assez fort/solide", "dyndns_set_recovery_password_failed": "Échec de la définition du mot de passe de récupération : {erreur}", - "dyndns_set_recovery_password_success": "Mot de passe de récupération défini/configuré !", - "log_dyndns_unsubscribe": "Se désabonner d'un sous-domaine YunoHost '{}'" + "dyndns_set_recovery_password_success": "Mot de passe de récupération changé !", + "log_dyndns_unsubscribe": "Se désabonner d'un sous-domaine YunoHost '{}'", + "dyndns_too_many_requests": "Le service dyndns de YunoHost a reçu trop de requêtes/demandes de votre part, attendez environ 1 heure avant de réessayer.", + "ask_dyndns_recovery_password_explain_unavailable": "Ce domaine DynDNS est déjà enregistré. Si vous êtes la personne qui a enregistré ce domaine lors de sa création, vous pouvez entrer le mot de passe de récupération pour récupérer ce domaine." } diff --git a/src/app.py b/src/app.py index 52e1ebede..1116feb73 100644 --- a/src/app.py +++ b/src/app.py @@ -244,6 +244,10 @@ def app_info(app, full=False, upgradable=False): ret["supports_config_panel"] = os.path.exists( os.path.join(setting_path, "config_panel.toml") ) + ret["supports_purge"] = ( + local_manifest["packaging_format"] >= 2 + and local_manifest["resources"].get("data_dir") is not None + ) ret["permissions"] = permissions ret["label"] = permissions.get(app + ".main", {}).get("label") @@ -2143,11 +2147,11 @@ def _parse_app_doc_and_notifications(path): def _hydrate_app_template(template, data): - # Apply jinja for stuff like {% if .. %} blocks, # but only if there's indeed an if block (to try to reduce overhead or idk) if "{%" in template: from jinja2 import Template + template = Template(template).render(**data) stuff_to_replace = set(re.findall(r"__[A-Z0-9]+?[A-Z0-9_]*?[A-Z0-9]*?__", template)) @@ -3071,7 +3075,9 @@ def _filter_and_hydrate_notifications(notifications, current_version=None, data= } # Filter out empty notifications (notifications may be empty because of if blocks) - return {name:content for name, content in out.items() if content and content.strip()} + return { + name: content for name, content in out.items() if content and content.strip() + } def _display_notifications(notifications, force=False): diff --git a/src/dns.py b/src/dns.py index 126f81dbf..6e5464092 100644 --- a/src/dns.py +++ b/src/dns.py @@ -548,8 +548,16 @@ def _get_registrar_config_section(domain): registrar_infos["registrar"]["default"] = "yunohost" registrar_infos["infos"]["style"] = "success" registrar_infos["infos"]["ask"] = m18n.n("domain_dns_registrar_yunohost") + registrar_infos["recovery_password"] = OrderedDict( + { + "type": "password", + "ask": m18n.n("ask_dyndns_recovery_password"), + "default": "", + } + ) return registrar_infos + elif is_special_use_tld(dns_zone): registrar_infos["infos"]["ask"] = m18n.n("domain_dns_conf_special_use_tld") diff --git a/src/domain.py b/src/domain.py index 7ac8a50cb..7cbf90ce1 100644 --- a/src/domain.py +++ b/src/domain.py @@ -381,6 +381,7 @@ def domain_remove( dyndns_recovery_password -- Recovery password used at the creation of the DynDNS domain ignore_dyndns -- If we just remove the DynDNS domain, without unsubscribing """ + import glob from yunohost.hook import hook_callback from yunohost.app import app_ssowatconf, app_info, app_remove from yunohost.utils.ldap import _get_ldap_interface @@ -466,14 +467,20 @@ def domain_remove( global domain_list_cache domain_list_cache = [] - stuff_to_delete = [ - f"/etc/yunohost/certs/{domain}", - f"/etc/yunohost/dyndns/K{domain}.+*", - f"{DOMAIN_SETTINGS_DIR}/{domain}.yml", - ] + # If a password is provided, delete the DynDNS record + if dyndns: + try: + # 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(stuff, force=True, recursive=True) + rm(f"/etc/yunohost/certs/{domain}", 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 # appears as manually modified even though they weren't touched ... @@ -501,13 +508,6 @@ def domain_remove( 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")) @@ -735,6 +735,19 @@ class DomainConfigPanel(ConfigPanel): domain=self.entity, 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 portal_options = [ "default_app", diff --git a/src/dyndns.py b/src/dyndns.py index 5c9e2a36e..34ae21c4d 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -27,7 +27,6 @@ from logging import getLogger from moulinette import Moulinette, m18n from moulinette.core import MoulinetteError 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.domain import _get_maindomain @@ -63,19 +62,28 @@ def _dyndns_available(domain): Returns: 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} ...") try: - r = download_json( - f"https://{DYNDNS_PROVIDER}/test/{domain}", expected_status_code=None - ) + r = requests.get(f"https://{DYNDNS_PROVIDER}/test/{domain}", timeout=30) except MoulinetteError as e: logger.error(str(e)) raise YunohostError( "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"]) @@ -94,14 +102,26 @@ def dyndns_subscribe(operation_logger, domain=None, recovery_password=None): "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 if not is_subscribing_allowed(): 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 if not recovery_password and Moulinette.interface.type == "cli": 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 regen_conf(["yunohost"]) elif r.status_code == 403: - raise YunohostError("dyndns_unsubscribe_denied") + raise YunohostValidationError("dyndns_unsubscribe_denied") 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: raise YunohostError( "dyndns_unsubscribe_failed", diff --git a/src/tests/test_domains.py b/src/tests/test_domains.py index 1bbbb7890..13a9f63b8 100644 --- a/src/tests/test_domains.py +++ b/src/tests/test_domains.py @@ -1,8 +1,10 @@ import pytest import os -import time import random +from mock import patch + +from moulinette import Moulinette from moulinette.core import MoulinetteError from yunohost.utils.error import YunohostError, YunohostValidationError @@ -75,11 +77,44 @@ 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 +def test_domain_add_and_remove_dyndns(): + # 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"] domain_add(TEST_DYNDNS_DOMAIN, dyndns_recovery_password=TEST_DYNDNS_PASSWORD) 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(): @@ -94,13 +129,6 @@ 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 diff --git a/src/tools.py b/src/tools.py index 23edf1004..d0e661c6c 100644 --- a/src/tools.py +++ b/src/tools.py @@ -156,7 +156,7 @@ def tools_postinstall( force_diskspace=False, 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.password import ( assert_password_is_strong_enough, @@ -218,7 +218,14 @@ def tools_postinstall( ) else: 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: raise YunohostValidationError( diff --git a/src/utils/dns.py b/src/utils/dns.py index b3ca4b564..569dad466 100644 --- a/src/utils/dns.py +++ b/src/utils/dns.py @@ -21,7 +21,7 @@ from typing import List from moulinette.utils.filesystem import read_file -SPECIAL_USE_TLDS = ["local", "localhost", "onion", "test"] +SPECIAL_USE_TLDS = ["home.arpa", "local", "localhost", "onion", "test"] YNH_DYNDNS_DOMAINS = ["nohost.me", "noho.st", "ynh.fr"] diff --git a/src/utils/resources.py b/src/utils/resources.py index 703262d71..ab603086d 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -61,10 +61,10 @@ class AppResourceManager: try: if todo == "deprovision": # FIXME : i18n, better info strings - logger.info(f"Deprovisionning {name}...") + logger.info(f"Deprovisioning {name}...") old.deprovision(context=context) elif todo == "provision": - logger.info(f"Provisionning {name}...") + logger.info(f"Provisioning {name}...") new.provision_or_update(context=context) elif todo == "update": logger.info(f"Updating {name}...") @@ -90,10 +90,10 @@ class AppResourceManager: # (NB. here we want to undo the todo) if todo == "deprovision": # FIXME : i18n, better info strings - logger.info(f"Reprovisionning {name}...") + logger.info(f"Reprovisioning {name}...") old.provision_or_update(context=context) elif todo == "provision": - logger.info(f"Deprovisionning {name}...") + logger.info(f"Deprovisioning {name}...") new.deprovision(context=context) elif todo == "update": logger.info(f"Reverting {name}...")