Merge remote-tracking branch 'origin/dev' into bookworm

This commit is contained in:
axolotle 2023-10-31 02:57:26 +01:00
commit bfba939927
18 changed files with 234 additions and 55 deletions

View file

@ -20,9 +20,11 @@ jobs:
# Setting up Git user # Setting up Git user
git config --global user.name 'yunohost-bot' git config --global user.name 'yunohost-bot'
git config --global user.email 'yunohost-bot@users.noreply.github.com' 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 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 [[ -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 - name: Commit changes
id: commit id: commit
if: ${{ env.PROCEED == 'true' }} if: ${{ env.PROCEED == 'true' }}
@ -34,14 +36,14 @@ jobs:
uses: peter-evans/create-pull-request@v3 uses: peter-evans/create-pull-request@v3
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
commit-message: Update n to version ${{ env.VERSION }} commit-message: Update n to ${{ env.VERSION }}
committer: 'yunohost-bot <yunohost-bot@users.noreply.github.com>' committer: 'yunohost-bot <yunohost-bot@users.noreply.github.com>'
author: 'yunohost-bot <yunohost-bot@users.noreply.github.com>' author: 'yunohost-bot <yunohost-bot@users.noreply.github.com>'
signoff: false signoff: false
base: dev base: dev
branch: ci-auto-update-n-v${{ env.VERSION }} branch: ci-auto-update-n-v${{ env.VERSION }}
delete-branch: true delete-branch: true
title: 'Upgrade n to version ${{ env.VERSION }}' title: 'Upgrade n to ${{ env.VERSION }}'
body: | body: |
Upgrade `n` to v${{ env.VERSION }} Upgrade `n` to ${{ env.VERSION }}
draft: false draft: false

View file

@ -118,8 +118,8 @@ plugin {
antispam_debug_target = syslog antispam_debug_target = syslog
antispam_verbose_debug = 0 antispam_verbose_debug = 0
antispam_backend = pipe antispam_backend = pipe
antispam_spam = Junk;SPAM antispam_spam_pattern_ignorecase = junk;spam
antispam_trash = Trash antispam_trash_pattern_ignorecase = trash;papierkorb;deleted messages
antispam_pipe_program = /usr/bin/rspamc antispam_pipe_program = /usr/bin/rspamc
antispam_pipe_program_args = -h;localhost:11334;-P;q1 antispam_pipe_program_args = -h;localhost:11334;-P;q1
antispam_pipe_program_spam_arg = learn_spam antispam_pipe_program_spam_arg = learn_spam

2
conf/rspamd/redis.conf Normal file
View file

@ -0,0 +1,2 @@
# set redis server
servers = "127.0.0.1";

18
debian/changelog vendored
View file

@ -4,6 +4,24 @@ yunohost (12.0.0) unstable; urgency=low
-- Alexandre Aubin <alex.aubin@mailoo.org> Thu, 04 May 2023 20:30:19 +0200 -- Alexandre Aubin <alex.aubin@mailoo.org> 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 <alex.aubin@mailoo.org> Mon, 09 Oct 2023 23:16:13 +0200
yunohost (11.2.4) stable; urgency=low yunohost (11.2.4) stable; urgency=low
- doc: Improve --help for 'yunohost app install' ([#1702](https://github.com/yunohost/yunohost/pull/1702)) - doc: Improve --help for 'yunohost app install' ([#1702](https://github.com/yunohost/yunohost/pull/1702))

View file

@ -250,7 +250,7 @@ ynh_install_app_dependencies() {
# Check for specific php dependencies which requires sury # 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" # This grep will for example return "7.4" if dependencies is "foo bar php7.4-pwet php-gni"
# The (?<=php) syntax corresponds to lookbehind ;) # 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" ]] if [[ -n "$specific_php_version" ]]
then then

61
helpers/vendor/n/n vendored
View file

@ -61,7 +61,7 @@ function n_grep() {
# Setup and state # Setup and state
# #
VERSION="v9.1.0" VERSION="v9.2.0"
N_PREFIX="${N_PREFIX-/usr/local}" N_PREFIX="${N_PREFIX-/usr/local}"
N_PREFIX=${N_PREFIX%/} 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) DOWNLOAD=false # set to opt-out of activate (install), and opt-in to download (run, exec)
ARCH= ARCH=
SHOW_VERBOSE_LOG="true" SHOW_VERBOSE_LOG="true"
OFFLINE=false
# ANSI escape codes # ANSI escape codes
# https://en.wikipedia.org/wiki/ANSI_escape_code # 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. -q, --quiet Disable curl output. Disable log messages processing "auto" and "engine" labels.
-d, --download Download if necessary, and don't make active -d, --download Download if necessary, and don't make active
-a, --arch Override system architecture -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 --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) --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. --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 exit
fi fi
fi fi
if [[ "$OFFLINE" == "true" ]]; then
abort "version unavailable offline"
fi
log installing "${g_mirror_folder_name}-v$version" log installing "${g_mirror_folder_name}-v$version"
@ -1103,6 +1108,7 @@ function get_package_engine_version() {
verbose_log "target" "${version}" verbose_log "target" "${version}"
else else
command -v npx &> /dev/null || abort "an active version of npx is required to use complex 'engine' ranges from package.json" 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}" verbose_log "resolving" "${range}"
local version_per_line="$(n lsr --all)" local version_per_line="$(n lsr --all)"
local versions_one_line=$(echo "${version_per_line}" | tr '\n' ' ') 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. # Just numbers, already resolved, no need to lookup first.
simple_version="${simple_version#v}" simple_version="${simple_version#v}"
g_target_node="${simple_version}" g_target_node="${simple_version}"
elif [[ "$OFFLINE" == "true" ]]; then
g_target_node=$(display_local_versions "${version}")
else else
# Complicated recognising exact version, KISS and lookup. # Complicated recognising exact version, KISS and lookup.
g_target_node=$(N_MAX_REMOTE_MATCHES=1 display_remote_versions "$version") g_target_node=$(N_MAX_REMOTE_MATCHES=1 display_remote_versions "$version")
@ -1232,6 +1240,56 @@ function display_match_limit(){
fi 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 # Synopsis: display_remote_versions version
# #
@ -1577,6 +1635,7 @@ while [[ $# -ne 0 ]]; do
-h|--help|help) display_help; exit ;; -h|--help|help) display_help; exit ;;
-q|--quiet) set_quiet ;; -q|--quiet) set_quiet ;;
-d|--download) DOWNLOAD="true" ;; -d|--download) DOWNLOAD="true" ;;
--offline) OFFLINE="true" ;;
--insecure) set_insecure ;; --insecure) set_insecure ;;
-p|--preserve) N_PRESERVE_NPM="true" N_PRESERVE_COREPACK="true" ;; -p|--preserve) N_PRESERVE_NPM="true" N_PRESERVE_COREPACK="true" ;;
--no-preserve) N_PRESERVE_NPM="" N_PRESERVE_COREPACK="" ;; --no-preserve) N_PRESERVE_NPM="" N_PRESERVE_COREPACK="" ;;

View file

@ -13,6 +13,8 @@ do_pre_regen() {
"${pending_dir}/etc/rspamd/local.d/dkim_signing.conf" "${pending_dir}/etc/rspamd/local.d/dkim_signing.conf"
install -D -m 644 rspamd.sieve \ install -D -m 644 rspamd.sieve \
"${pending_dir}/etc/dovecot/global_script/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() { do_post_regen() {

View file

@ -91,6 +91,7 @@
"ask_new_path": "New path", "ask_new_path": "New path",
"ask_password": "Password", "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": "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": "DynDNS recovery password",
"ask_dyndns_recovery_password_explain_during_unsubscribe": "Please enter the recovery password for this DynDNS domain.", "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", "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_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_subscribed": "DynDNS domain subscribed",
"dyndns_subscribe_failed": "Could not subscribe DynDNS domain: {error}", "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_unsubscribe_failed": "Could not unsubscribe DynDNS domain: {error}",
"dyndns_unsubscribed": "DynDNS domain unsubscribed", "dyndns_unsubscribed": "DynDNS domain unsubscribed",
"dyndns_unsubscribe_denied": "Failed to unsubscribe domain: invalid credentials", "dyndns_unsubscribe_denied": "Failed to unsubscribe domain: invalid credentials",

View file

@ -766,5 +766,13 @@
"group_mailalias_add": "'{mail}' ePosta aliasa jarri zaio '{group}' taldeari", "group_mailalias_add": "'{mail}' ePosta aliasa jarri zaio '{group}' taldeari",
"group_mailalias_remove": "'{mail}' ePosta aliasa kendu zaio '{group}' taldeari", "group_mailalias_remove": "'{mail}' ePosta aliasa kendu zaio '{group}' taldeari",
"group_user_remove": "'{user}' erabiltzailea '{group}' taldetik kenduko da", "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"
} }

View file

@ -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_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_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_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é !", "dyndns_set_recovery_password_success": "Mot de passe de récupération changé !",
"log_dyndns_unsubscribe": "Se désabonner d'un sous-domaine YunoHost '{}'" "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."
} }

View file

@ -244,6 +244,10 @@ def app_info(app, full=False, upgradable=False):
ret["supports_config_panel"] = os.path.exists( ret["supports_config_panel"] = os.path.exists(
os.path.join(setting_path, "config_panel.toml") 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["permissions"] = permissions
ret["label"] = permissions.get(app + ".main", {}).get("label") 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): def _hydrate_app_template(template, data):
# Apply jinja for stuff like {% if .. %} blocks, # Apply jinja for stuff like {% if .. %} blocks,
# but only if there's indeed an if block (to try to reduce overhead or idk) # but only if there's indeed an if block (to try to reduce overhead or idk)
if "{%" in template: if "{%" in template:
from jinja2 import Template from jinja2 import Template
template = Template(template).render(**data) template = Template(template).render(**data)
stuff_to_replace = set(re.findall(r"__[A-Z0-9]+?[A-Z0-9_]*?[A-Z0-9]*?__", template)) 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) # 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): def _display_notifications(notifications, force=False):

View file

@ -548,8 +548,16 @@ def _get_registrar_config_section(domain):
registrar_infos["registrar"]["default"] = "yunohost" registrar_infos["registrar"]["default"] = "yunohost"
registrar_infos["infos"]["style"] = "success" registrar_infos["infos"]["style"] = "success"
registrar_infos["infos"]["ask"] = m18n.n("domain_dns_registrar_yunohost") 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 return registrar_infos
elif is_special_use_tld(dns_zone): elif is_special_use_tld(dns_zone):
registrar_infos["infos"]["ask"] = m18n.n("domain_dns_conf_special_use_tld") registrar_infos["infos"]["ask"] = m18n.n("domain_dns_conf_special_use_tld")

View file

@ -381,6 +381,7 @@ def domain_remove(
dyndns_recovery_password -- Recovery password used at the creation of the DynDNS domain dyndns_recovery_password -- Recovery password used at the creation of the DynDNS domain
ignore_dyndns -- If we just remove the DynDNS domain, without unsubscribing ignore_dyndns -- If we just remove the DynDNS domain, without unsubscribing
""" """
import glob
from yunohost.hook import hook_callback from yunohost.hook import hook_callback
from yunohost.app import app_ssowatconf, app_info, app_remove from yunohost.app import app_ssowatconf, app_info, app_remove
from yunohost.utils.ldap import _get_ldap_interface from yunohost.utils.ldap import _get_ldap_interface
@ -466,14 +467,20 @@ def domain_remove(
global domain_list_cache global domain_list_cache
domain_list_cache = [] domain_list_cache = []
stuff_to_delete = [ # If a password is provided, delete the DynDNS record
f"/etc/yunohost/certs/{domain}", if dyndns:
f"/etc/yunohost/dyndns/K{domain}.+*", try:
f"{DOMAIN_SETTINGS_DIR}/{domain}.yml", # 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(f"/etc/yunohost/certs/{domain}", force=True, recursive=True)
rm(stuff, 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 # Sometime we have weird issues with the regenconf where some files
# appears as manually modified even though they weren't touched ... # appears as manually modified even though they weren't touched ...
@ -501,13 +508,6 @@ def domain_remove(
hook_callback("post_domain_remove", args=[domain]) 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")) logger.success(m18n.n("domain_deleted"))
@ -735,6 +735,19 @@ class DomainConfigPanel(ConfigPanel):
domain=self.entity, domain=self.entity,
other_app=app_map(raw=True)[self.entity]["/"]["id"], 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 = [ portal_options = [
"default_app", "default_app",

View file

@ -27,7 +27,6 @@ from logging import getLogger
from moulinette import Moulinette, m18n from moulinette import Moulinette, m18n
from moulinette.core import MoulinetteError from moulinette.core import MoulinetteError
from moulinette.utils.filesystem import write_to_file, rm, chown, chmod 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.utils.error import YunohostError, YunohostValidationError
from yunohost.domain import _get_maindomain from yunohost.domain import _get_maindomain
@ -63,19 +62,28 @@ def _dyndns_available(domain):
Returns: Returns:
True if the domain is available, False otherwise. 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} ...") logger.debug(f"Checking if domain {domain} is available on {DYNDNS_PROVIDER} ...")
try: try:
r = download_json( r = requests.get(f"https://{DYNDNS_PROVIDER}/test/{domain}", timeout=30)
f"https://{DYNDNS_PROVIDER}/test/{domain}", expected_status_code=None
)
except MoulinetteError as e: except MoulinetteError as e:
logger.error(str(e)) logger.error(str(e))
raise YunohostError( raise YunohostError(
"dyndns_could_not_check_available", domain=domain, provider=DYNDNS_PROVIDER "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"]) @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 "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 # Check adding another dyndns domain is still allowed
if not is_subscribing_allowed(): if not is_subscribing_allowed():
raise YunohostValidationError("domain_dyndns_already_subscribed") 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 # Prompt for a password if running in CLI and no password provided
if not recovery_password and Moulinette.interface.type == "cli": if not recovery_password and Moulinette.interface.type == "cli":
logger.warning(m18n.n("ask_dyndns_recovery_password_explain")) 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 # in /etc/yunohost/dyndns
regen_conf(["yunohost"]) regen_conf(["yunohost"])
elif r.status_code == 403: elif r.status_code == 403:
raise YunohostError("dyndns_unsubscribe_denied") raise YunohostValidationError("dyndns_unsubscribe_denied")
elif r.status_code == 409: 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: else:
raise YunohostError( raise YunohostError(
"dyndns_unsubscribe_failed", "dyndns_unsubscribe_failed",

View file

@ -1,8 +1,10 @@
import pytest import pytest
import os import os
import time
import random import random
from mock import patch
from moulinette import Moulinette
from moulinette.core import MoulinetteError from moulinette.core import MoulinetteError
from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.utils.error import YunohostError, YunohostValidationError
@ -75,11 +77,44 @@ def test_domain_add():
assert TEST_DOMAINS[2] in domain_list()["domains"] assert TEST_DOMAINS[2] in domain_list()["domains"]
def test_domain_add_subscribe(): def test_domain_add_and_remove_dyndns():
time.sleep(35) # Dynette blocks requests that happen too frequently # 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"] assert TEST_DYNDNS_DOMAIN not in domain_list()["domains"]
domain_add(TEST_DYNDNS_DOMAIN, dyndns_recovery_password=TEST_DYNDNS_PASSWORD) domain_add(TEST_DYNDNS_DOMAIN, dyndns_recovery_password=TEST_DYNDNS_PASSWORD)
assert TEST_DYNDNS_DOMAIN in domain_list()["domains"] 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(): def test_domain_add_existing_domain():
@ -94,13 +129,6 @@ def test_domain_remove():
assert TEST_DOMAINS[1] not in domain_list()["domains"] 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(): def test_main_domain():
current_main_domain = _get_maindomain() current_main_domain = _get_maindomain()
assert domain_main_domain()["current_main_domain"] == current_main_domain assert domain_main_domain()["current_main_domain"] == current_main_domain

View file

@ -156,7 +156,7 @@ def tools_postinstall(
force_diskspace=False, force_diskspace=False,
overwrite_root_password=True, 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.dns import is_yunohost_dyndns_domain
from yunohost.utils.password import ( from yunohost.utils.password import (
assert_password_is_strong_enough, assert_password_is_strong_enough,
@ -218,6 +218,13 @@ def tools_postinstall(
) )
else: else:
if not available: if not available:
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) raise YunohostValidationError("dyndns_unavailable", domain=domain)
if os.system("iptables -V >/dev/null 2>/dev/null") != 0: if os.system("iptables -V >/dev/null 2>/dev/null") != 0:

View file

@ -21,7 +21,7 @@ from typing import List
from moulinette.utils.filesystem import read_file 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"] YNH_DYNDNS_DOMAINS = ["nohost.me", "noho.st", "ynh.fr"]

View file

@ -61,10 +61,10 @@ class AppResourceManager:
try: try:
if todo == "deprovision": if todo == "deprovision":
# FIXME : i18n, better info strings # FIXME : i18n, better info strings
logger.info(f"Deprovisionning {name}...") logger.info(f"Deprovisioning {name}...")
old.deprovision(context=context) old.deprovision(context=context)
elif todo == "provision": elif todo == "provision":
logger.info(f"Provisionning {name}...") logger.info(f"Provisioning {name}...")
new.provision_or_update(context=context) new.provision_or_update(context=context)
elif todo == "update": elif todo == "update":
logger.info(f"Updating {name}...") logger.info(f"Updating {name}...")
@ -90,10 +90,10 @@ class AppResourceManager:
# (NB. here we want to undo the todo) # (NB. here we want to undo the todo)
if todo == "deprovision": if todo == "deprovision":
# FIXME : i18n, better info strings # FIXME : i18n, better info strings
logger.info(f"Reprovisionning {name}...") logger.info(f"Reprovisioning {name}...")
old.provision_or_update(context=context) old.provision_or_update(context=context)
elif todo == "provision": elif todo == "provision":
logger.info(f"Deprovisionning {name}...") logger.info(f"Deprovisioning {name}...")
new.deprovision(context=context) new.deprovision(context=context)
elif todo == "update": elif todo == "update":
logger.info(f"Reverting {name}...") logger.info(f"Reverting {name}...")