Merge branch 'unstable' into tsig-sha256

This commit is contained in:
Laurent Peuch 2018-01-03 19:48:38 +01:00
commit 34433f07a4
17 changed files with 416 additions and 171 deletions

View file

@ -6,7 +6,7 @@
# #
# If no argument provided, a standard directory will be use. /var/log/${app} # If no argument provided, a standard directory will be use. /var/log/${app}
# You can provide a path with the directory only or with the logfile. # You can provide a path with the directory only or with the logfile.
# /parentdir/logdir/ # /parentdir/logdir
# /parentdir/logdir/logfile.log # /parentdir/logdir/logfile.log
# #
# It's possible to use this helper several times, each config will be added to the same logrotate config file. # It's possible to use this helper several times, each config will be added to the same logrotate config file.
@ -24,7 +24,7 @@ ynh_use_logrotate () {
if [ "$(echo ${1##*.})" == "log" ]; then # Keep only the extension to check if it's a logfile if [ "$(echo ${1##*.})" == "log" ]; then # Keep only the extension to check if it's a logfile
logfile=$1 # In this case, focus logrotate on the logfile logfile=$1 # In this case, focus logrotate on the logfile
else else
logfile=$1/.log # Else, uses the directory and all logfile into it. logfile=$1/*.log # Else, uses the directory and all logfile into it.
fi fi
else else
logfile="/var/log/${app}/*.log" # Without argument, use a defaut directory in /var/log logfile="/var/log/${app}/*.log" # Without argument, use a defaut directory in /var/log
@ -123,6 +123,9 @@ ynh_add_nginx_config () {
# To avoid a break by set -u, use a void substitution ${var:-}. If the variable is not set, it's simply set with an empty variable. # To avoid a break by set -u, use a void substitution ${var:-}. If the variable is not set, it's simply set with an empty variable.
# Substitute in a nginx config file only if the variable is not empty # Substitute in a nginx config file only if the variable is not empty
if test -n "${path_url:-}"; then if test -n "${path_url:-}"; then
# path_url_slash_less is path_url, or a blank value if path_url is only '/'
path_url_slash_less=${path_url%/}
ynh_replace_string "__PATH__/" "$path_url_slash_less/" "$finalnginxconf"
ynh_replace_string "__PATH__" "$path_url" "$finalnginxconf" ynh_replace_string "__PATH__" "$path_url" "$finalnginxconf"
fi fi
if test -n "${domain:-}"; then if test -n "${domain:-}"; then

View file

@ -73,7 +73,7 @@ ynh_mysql_drop_db() {
# | arg: db - the database name to dump # | arg: db - the database name to dump
# | ret: the mysqldump output # | ret: the mysqldump output
ynh_mysql_dump_db() { ynh_mysql_dump_db() {
mysqldump -u "root" -p"$(sudo cat $MYSQL_ROOT_PWD_FILE)" "$1" mysqldump -u "root" -p"$(sudo cat $MYSQL_ROOT_PWD_FILE)" --single-transaction --skip-dump-date "$1"
} }
# Create a user # Create a user

View file

@ -126,9 +126,6 @@ ynh_install_app_dependencies () {
version=$(grep '\"version\": ' "$manifest_path" | cut -d '"' -f 4) # Retrieve the version number in the manifest file. version=$(grep '\"version\": ' "$manifest_path" | cut -d '"' -f 4) # Retrieve the version number in the manifest file.
dep_app=${app//_/-} # Replace all '_' by '-' dep_app=${app//_/-} # Replace all '_' by '-'
if ynh_package_is_installed "${dep_app}-ynh-deps"; then
echo "A package named ${dep_app}-ynh-deps is already installed" >&2
else
cat > /tmp/${dep_app}-ynh-deps.control << EOF # Make a control file for equivs-build cat > /tmp/${dep_app}-ynh-deps.control << EOF # Make a control file for equivs-build
Section: misc Section: misc
Priority: optional Priority: optional
@ -143,7 +140,6 @@ EOF
|| ynh_die "Unable to install dependencies" # Install the fake package and its dependencies || ynh_die "Unable to install dependencies" # Install the fake package and its dependencies
rm /tmp/${dep_app}-ynh-deps.control rm /tmp/${dep_app}-ynh-deps.control
ynh_app_setting_set $app apt_dependencies $dependencies ynh_app_setting_set $app apt_dependencies $dependencies
fi
} }
# Remove fake package and its dependencies # Remove fake package and its dependencies

View file

@ -10,17 +10,50 @@ ynh_string_random() {
| sed -n 's/\(.\{'"${1:-24}"'\}\).*/\1/p' | sed -n 's/\(.\{'"${1:-24}"'\}\).*/\1/p'
} }
# Substitute/replace a string by another in a file # Substitute/replace a string (or expression) by another in a file
# #
# usage: ynh_replace_string match_string replace_string target_file # usage: ynh_replace_string match_string replace_string target_file
# | arg: match_string - String to be searched and replaced in the file # | arg: match_string - String to be searched and replaced in the file
# | arg: replace_string - String that will replace matches # | arg: replace_string - String that will replace matches
# | arg: target_file - File in which the string will be replaced. # | arg: target_file - File in which the string will be replaced.
#
# As this helper is based on sed command, regular expressions and
# references to sub-expressions can be used
# (see sed manual page for more information)
ynh_replace_string () { ynh_replace_string () {
delimit=@ local delimit=@
match_string=${1//${delimit}/"\\${delimit}"} # Escape the delimiter if it's in the string. local match_string=$1
replace_string=${2//${delimit}/"\\${delimit}"} local replace_string=$2
workfile=$3 local workfile=$3
# Escape the delimiter if it's in the string.
match_string=${match_string//${delimit}/"\\${delimit}"}
replace_string=${replace_string//${delimit}/"\\${delimit}"}
sudo sed --in-place "s${delimit}${match_string}${delimit}${replace_string}${delimit}g" "$workfile" sudo sed --in-place "s${delimit}${match_string}${delimit}${replace_string}${delimit}g" "$workfile"
} }
# Substitute/replace a special string by another in a file
#
# usage: ynh_replace_special_string match_string replace_string target_file
# | arg: match_string - String to be searched and replaced in the file
# | arg: replace_string - String that will replace matches
# | arg: target_file - File in which the string will be replaced.
#
# This helper will use ynh_replace_string, but as you can use special
# characters, you can't use some regular expressions and sub-expressions.
ynh_replace_special_string () {
local match_string=$1
local replace_string=$2
local workfile=$3
# Escape any backslash to preserve them as simple backslash.
match_string=${match_string//\\/"\\\\"}
replace_string=${replace_string//\\/"\\\\"}
# Escape the & character, who has a special function in sed.
match_string=${match_string//&/"\&"}
replace_string=${replace_string//&/"\&"}
ynh_replace_string "$match_string" "$replace_string" "$workfile"
}

View file

@ -59,6 +59,11 @@ ynh_restore_upgradebackup () {
# ynh_abort_if_errors # ynh_abort_if_errors
# #
ynh_backup_before_upgrade () { ynh_backup_before_upgrade () {
if [ ! -e "/etc/yunohost/apps/$app/scripts/backup" ]
then
echo "This app doesn't have any backup script." >&2
return
fi
backup_number=1 backup_number=1
old_backup_number=2 old_backup_number=2
app_bck=${app//_/-} # Replace all '_' by '-' app_bck=${app//_/-} # Replace all '_' by '-'

View file

@ -1,7 +1,7 @@
uPnP: uPnP:
enabled: false enabled: false
TCP: [22, 25, 53, 80, 443, 465, 587, 993, 5222, 5269] TCP: [22, 25, 80, 443, 465, 587, 993, 5222, 5269]
UDP: [53] UDP: []
ipv4: ipv4:
TCP: [22, 25, 53, 80, 443, 465, 587, 993, 5222, 5269] TCP: [22, 25, 53, 80, 443, 465, 587, 993, 5222, 5269]
UDP: [53, 5353] UDP: [53, 5353]

View file

@ -47,6 +47,7 @@ yunohost-api:
log: /var/log/yunohost/yunohost-api.log log: /var/log/yunohost/yunohost-api.log
yunohost-firewall: yunohost-firewall:
status: service status: service
need_lock: true
nslcd: nslcd:
status: service status: service
log: /var/log/syslog log: /var/log/syslog

45
debian/changelog vendored
View file

@ -1,3 +1,48 @@
yunohost (2.7.5) stable; urgency=low
(Bumping version number for stable release)
-- Alexandre Aubin <alex.aubin@mailoo.org> Sat, 02 Dec 2017 12:38:00 -0500
yunohost (2.7.4) testing; urgency=low
* [fix] Update acme-tiny as LE updated its ToS (#386)
* [fix] Fix helper for old apps without backup script (#388)
* [mod] Remove port 53 from UPnP (but keep it open on local network) (#362)
* [i18n] Improve French translation
Thanks to all contributors <3 ! (jibec, Moul, Maniack, Aleks)
-- Alexandre Aubin <alex.aubin@mailoo.org> Tue, 28 Nov 2017 19:01:41 -0500
yunohost (2.7.3) testing; urgency=low
Major changes :
* [fix] Refactor/clean madness related to DynDNS (#353)
* [i18n] Improve french translation (#355)
* [fix] Use cryptorandom to generate password (#358)
* [enh] Support for single app upgrade from the webadmin (#359)
* [enh] Be able to give lock to son processes detached by systemctl (#367)
* [enh] Make MySQL dumps with a single transaction to ensure backup consistency (#370)
Misc fixes/improvements :
* [enh] Escape some special character in ynh_replace_string (#354)
* [fix] Allow dash at the beginning of app settings value (#357)
* [enh] Handle root path in nginx conf (#361)
* [enh] Add debugging in ldap init (#365)
* [fix] Fix app_upgrade_string with missing key
* [fix] Fix for change_url path normalizing with root url (#368)
* [fix] Missing 'ask_path' string (#369)
* [enh] Remove date from sql dump (#371)
* [fix] Fix unicode error in backup/restore (#375)
* [fix] Fix an error in ynh_replace_string (#379)
Thanks to all contributors <3 ! (Bram, Maniack C, ljf, JimboJoe, ariasuni, Jibec, Aleks)
-- Alexandre Aubin <alex.aubin@mailoo.org> Thu, 12 Oct 2017 16:18:51 -0400
yunohost (2.7.2) stable; urgency=low yunohost (2.7.2) stable; urgency=low
* [mod] pep8 * [mod] pep8

View file

@ -55,6 +55,7 @@
"ask_main_domain": "Main domain", "ask_main_domain": "Main domain",
"ask_new_admin_password": "New administration password", "ask_new_admin_password": "New administration password",
"ask_password": "Password", "ask_password": "Password",
"ask_path": "Path",
"backup_abstract_method": "This backup method hasn't yet been implemented", "backup_abstract_method": "This backup method hasn't yet been implemented",
"backup_action_required": "You must specify something to save", "backup_action_required": "You must specify something to save",
"backup_app_failed": "Unable to back up the app '{app:s}'", "backup_app_failed": "Unable to back up the app '{app:s}'",
@ -157,6 +158,7 @@
"domains_available": "Available domains:", "domains_available": "Available domains:",
"done": "Done", "done": "Done",
"downloading": "Downloading...", "downloading": "Downloading...",
"dyndns_could_not_check_provide": "Could not check if {provider:s} can provide {domain:s}.",
"dyndns_cron_installed": "The DynDNS cron job has been installed", "dyndns_cron_installed": "The DynDNS cron job has been installed",
"dyndns_cron_remove_failed": "Unable to remove the DynDNS cron job", "dyndns_cron_remove_failed": "Unable to remove the DynDNS cron job",
"dyndns_cron_removed": "The DynDNS cron job has been removed", "dyndns_cron_removed": "The DynDNS cron job has been removed",
@ -167,7 +169,8 @@
"dyndns_no_domain_registered": "No domain has been registered with DynDNS", "dyndns_no_domain_registered": "No domain has been registered with DynDNS",
"dyndns_registered": "The DynDNS domain has been registered", "dyndns_registered": "The DynDNS domain has been registered",
"dyndns_registration_failed": "Unable to register DynDNS domain: {error:s}", "dyndns_registration_failed": "Unable to register DynDNS domain: {error:s}",
"dyndns_unavailable": "Unavailable DynDNS subdomain", "dyndns_domain_not_provided": "Dyndns provider {provider:s} cannot provide domain {domain:s}.",
"dyndns_unavailable": "Domain {domain:s} is not available.",
"executing_command": "Executing command '{command:s}'...", "executing_command": "Executing command '{command:s}'...",
"executing_script": "Executing script '{script:s}'...", "executing_script": "Executing script '{script:s}'...",
"extracting": "Extracting...", "extracting": "Extracting...",

View file

@ -1,7 +1,7 @@
{ {
"action_invalid": "Action « {action:s} » incorrecte", "action_invalid": "Action « {action:s} » incorrecte",
"admin_password": "Mot de passe d'administration", "admin_password": "Mot de passe d'administration",
"admin_password_change_failed": "Impossible de modifier le mot de passe d'administration", "admin_password_change_failed": "Impossible de changer le mot de passe",
"admin_password_changed": "Le mot de passe d'administration a été modifié", "admin_password_changed": "Le mot de passe d'administration a été modifié",
"app_already_installed": "{app:s} est déjà installé", "app_already_installed": "{app:s} est déjà installé",
"app_argument_choice_invalid": "Choix invalide pour le paramètre « {name:s} », il doit être l'un de {choices:s}", "app_argument_choice_invalid": "Choix invalide pour le paramètre « {name:s} », il doit être l'un de {choices:s}",
@ -14,7 +14,7 @@
"app_install_files_invalid": "Fichiers d'installation incorrects", "app_install_files_invalid": "Fichiers d'installation incorrects",
"app_location_already_used": "Une application est déjà installée à cet emplacement", "app_location_already_used": "Une application est déjà installée à cet emplacement",
"app_location_install_failed": "Impossible d'installer l'application à cet emplacement", "app_location_install_failed": "Impossible d'installer l'application à cet emplacement",
"app_manifest_invalid": "Manifeste d'application incorrect", "app_manifest_invalid": "Manifeste d'application incorrect : {error}",
"app_no_upgrade": "Aucune application à mettre à jour", "app_no_upgrade": "Aucune application à mettre à jour",
"app_not_correctly_installed": "{app:s} semble être mal installé", "app_not_correctly_installed": "{app:s} semble être mal installé",
"app_not_installed": "{app:s} n'est pas installé", "app_not_installed": "{app:s} n'est pas installé",
@ -98,7 +98,7 @@
"dyndns_no_domain_registered": "Aucun domaine n'a été enregistré avec DynDNS", "dyndns_no_domain_registered": "Aucun domaine n'a été enregistré avec DynDNS",
"dyndns_registered": "Le domaine DynDNS a été enregistré", "dyndns_registered": "Le domaine DynDNS a été enregistré",
"dyndns_registration_failed": "Impossible d'enregistrer le domaine DynDNS : {error:s}", "dyndns_registration_failed": "Impossible d'enregistrer le domaine DynDNS : {error:s}",
"dyndns_unavailable": "Sous-domaine DynDNS indisponible", "dyndns_unavailable": "Le domaine {domain:s} est indisponible.",
"executing_command": "Exécution de la commande « {command:s} »...", "executing_command": "Exécution de la commande « {command:s} »...",
"executing_script": "Exécution du script « {script:s} »...", "executing_script": "Exécution du script « {script:s} »...",
"extracting": "Extraction...", "extracting": "Extraction...",
@ -320,7 +320,7 @@
"backup_archive_system_part_not_available": "La partie « {part:s} » du système nest pas disponible dans cette sauvegarde", "backup_archive_system_part_not_available": "La partie « {part:s} » du système nest pas disponible dans cette sauvegarde",
"backup_archive_mount_failed": "Le montage de larchive de sauvegarde a échoué", "backup_archive_mount_failed": "Le montage de larchive de sauvegarde a échoué",
"backup_archive_writing_error": "Impossible dajouter les fichiers à la sauvegarde dans larchive compressée", "backup_archive_writing_error": "Impossible dajouter les fichiers à la sauvegarde dans larchive compressée",
"backup_ask_for_copying_if_needed": "Votre système ne prend pas complètement en charge la méthode rapide dorganisation des fichiers dans larchive, voulez-vous les organiser en copiant {size:s} Mio ?", "backup_ask_for_copying_if_needed": "Certains fichiers nont pas pu être préparés pour être sauvegardée en utilisant la méthode qui évite de temporairement gaspiller de lespace sur le système. Pour mener la sauvegarde, {size:s} Mio doivent être temporairement utilisés. Acceptez-vous ?",
"backup_borg_not_implemented": "La méthode de sauvegarde Bord nest pas encore implémentée", "backup_borg_not_implemented": "La méthode de sauvegarde Bord nest pas encore implémentée",
"backup_cant_mount_uncompress_archive": "Impossible de monter en lecture seule le dossier de larchive décompressée", "backup_cant_mount_uncompress_archive": "Impossible de monter en lecture seule le dossier de larchive décompressée",
"backup_copying_to_organize_the_archive": "Copie de {size:s} Mio pour organiser larchive", "backup_copying_to_organize_the_archive": "Copie de {size:s} Mio pour organiser larchive",
@ -344,5 +344,28 @@
"restore_mounting_archive": "Montage de larchive dans « {path:s} »", "restore_mounting_archive": "Montage de larchive dans « {path:s} »",
"restore_may_be_not_enough_disk_space": "Votre système semble ne pas avoir suffisamment despace disponible (libre : {free_space:d} octets, nécessaire : {needed_space:d} octets, marge de sécurité : {margin:d} octets)", "restore_may_be_not_enough_disk_space": "Votre système semble ne pas avoir suffisamment despace disponible (libre : {free_space:d} octets, nécessaire : {needed_space:d} octets, marge de sécurité : {margin:d} octets)",
"restore_not_enough_disk_space": "Espace disponible insuffisant (libre : {free_space:d} octets, nécessaire : {needed_space:d} octets, marge de sécurité : {margin:d} octets)", "restore_not_enough_disk_space": "Espace disponible insuffisant (libre : {free_space:d} octets, nécessaire : {needed_space:d} octets, marge de sécurité : {margin:d} octets)",
"restore_system_part_failed": "Impossible de restaurer la partie « {part:s} » du système" "restore_system_part_failed": "Impossible de restaurer la partie « {part:s} » du système",
"backup_couldnt_bind": "Impossible de lier {src:s} avec {dest:s}.",
"domain_dns_conf_is_just_a_recommendation": "Cette page montre la configuration *recommandée*. Elle ne configure *pas* le DNS pour vous. Il est de votre responsabilité que de configurer votre zone DNS chez votre registrar DNS avec cette recommandation.",
"domain_dyndns_dynette_is_unreachable": "Impossible de contacter la dynette YunoHost, soit YunoHost nest pas correctement connecté à internet ou alors le serveur de dynette est arrêté. Erreur : {error}",
"migrations_backward": "Migration en arrière.",
"migrations_bad_value_for_target": "Nombre invalide pour le paramètre « target », les numéros de migration sont ou {}",
"migrations_cant_reach_migration_file": "Impossible daccéder aux fichiers de migrations avec le chemin %s",
"migrations_current_target": "La cible de migration est {}",
"migrations_error_failed_to_load_migration": "ERREUR : échec du chargement de migration {number} {name}",
"migrations_forward": "Migration en avant",
"migrations_loading_migration": "Chargement de la migration {number} {name}…",
"migrations_migration_has_failed": "La migration {number} {name} a échoué avec lexception {exception}, annulation",
"migrations_no_migrations_to_run": "Aucune migration à lancer",
"migrations_show_currently_running_migration": "Application de la migration {number} {name}…",
"migrations_show_last_migration": "La dernière migration appliquée est {}",
"migrations_skip_migration": "Omission de la migration {number} {name}…",
"server_shutdown": "Le serveur sera éteint",
"server_shutdown_confirm": "Le serveur immédiatement être éteint, le voulez-vous vraiment ? [{answers:s}]",
"server_reboot": "Le serveur va redémarrer",
"server_reboot_confirm": "Le serveur va redémarrer immédiatement, le voulez-vous vraiment ? [{answers:s}]",
"app_upgrade_some_app_failed": "Impossible de mettre à jour certaines applications",
"ask_path": "Chemin",
"dyndns_could_not_check_provide": "Impossible de vérifier si {provider:s} peut fournir {domain:s}.",
"dyndns_domain_not_provided": "Le fournisseur Dyndns {provider:s} ne peut pas fournir le domaine {domain:s}."
} }

View file

@ -38,7 +38,7 @@ import pwd
import grp import grp
from collections import OrderedDict from collections import OrderedDict
from moulinette import msignals, m18n from moulinette import msignals, m18n, msettings
from moulinette.core import MoulinetteError from moulinette.core import MoulinetteError
from moulinette.utils.log import getActionLogger from moulinette.utils.log import getActionLogger
@ -445,8 +445,9 @@ def app_change_url(auth, app, domain, path):
# Normalize path and domain format # Normalize path and domain format
domain = domain.strip().lower() domain = domain.strip().lower()
old_path = '/' + old_path.strip("/").strip() + '/'
path = '/' + path.strip("/").strip() + '/' old_path = normalize_url_path(old_path)
path = normalize_url_path(path)
if (domain, path) == (old_domain, old_path): if (domain, path) == (old_domain, old_path):
raise MoulinetteError(errno.EINVAL, m18n.n("app_change_url_identical_domains", domain=domain, path=path)) raise MoulinetteError(errno.EINVAL, m18n.n("app_change_url_identical_domains", domain=domain, path=path))
@ -531,6 +532,9 @@ def app_upgrade(auth, app=[], url=None, file=None):
""" """
from yunohost.hook import hook_add, hook_remove, hook_exec from yunohost.hook import hook_add, hook_remove, hook_exec
# Retrieve interface
is_api = msettings.get('interface') == 'api'
try: try:
app_list() app_list()
except MoulinetteError: except MoulinetteError:
@ -632,6 +636,10 @@ def app_upgrade(auth, app=[], url=None, file=None):
logger.success(m18n.n('upgrade_complete')) logger.success(m18n.n('upgrade_complete'))
# Return API logs if it is an API call
if is_api:
return {"log": service_log('yunohost-api', number="100").values()[0]}
def app_install(auth, app, label=None, args=None, no_remove_on_failure=False): def app_install(auth, app, label=None, args=None, no_remove_on_failure=False):
""" """
@ -2105,3 +2113,10 @@ def random_password(length=8):
char_set = string.ascii_uppercase + string.digits + string.ascii_lowercase char_set = string.ascii_uppercase + string.digits + string.ascii_lowercase
return ''.join([random.SystemRandom().choice(char_set) for x in range(length)]) return ''.join([random.SystemRandom().choice(char_set) for x in range(length)])
def normalize_url_path(url_path):
if url_path.strip("/").strip():
return '/' + url_path.strip("/").strip() + '/'
return "/"

View file

@ -166,11 +166,11 @@ class BackupRestoreTargetsManager(object):
or (exclude and isinstance(exclude, list) and not include) or (exclude and isinstance(exclude, list) and not include)
if include: if include:
return [target for target in self.targets[category] return [target.encode("Utf-8") for target in self.targets[category]
if self.results[category][target] in include] if self.results[category][target] in include]
if exclude: if exclude:
return [target for target in self.targets[category] return [target.encode("Utf-8") for target in self.targets[category]
if self.results[category][target] not in exclude] if self.results[category][target] not in exclude]

View file

@ -82,28 +82,23 @@ def domain_add(auth, domain, dyndns=False):
# DynDNS domain # DynDNS domain
if dyndns: if dyndns:
if len(domain.split('.')) < 3:
raise MoulinetteError(errno.EINVAL, m18n.n('domain_dyndns_invalid'))
# Do not allow to subscribe to multiple dyndns domains...
if os.path.exists('/etc/cron.d/yunohost-dyndns'): if os.path.exists('/etc/cron.d/yunohost-dyndns'):
raise MoulinetteError(errno.EPERM, raise MoulinetteError(errno.EPERM,
m18n.n('domain_dyndns_already_subscribed')) m18n.n('domain_dyndns_already_subscribed'))
try:
r = requests.get('https://dyndns.yunohost.org/domains', timeout=30)
except requests.ConnectionError as e:
raise MoulinetteError(errno.EHOSTUNREACH,
m18n.n('domain_dyndns_dynette_is_unreachable', error=str(e)))
from yunohost.dyndns import dyndns_subscribe from yunohost.dyndns import dyndns_subscribe, _dyndns_provides
dyndomains = json.loads(r.text) # Check that this domain can effectively be provided by
dyndomain = '.'.join(domain.split('.')[1:]) # dyndns.yunohost.org. (i.e. is it a nohost.me / noho.st)
if dyndomain in dyndomains: if not _dyndns_provides("dyndns.yunohost.org", domain):
dyndns_subscribe(domain=domain)
else:
raise MoulinetteError(errno.EINVAL, raise MoulinetteError(errno.EINVAL,
m18n.n('domain_dyndns_root_unknown')) m18n.n('domain_dyndns_root_unknown'))
# Actually subscribe
dyndns_subscribe(domain=domain)
try: try:
yunohost.certificate._certificate_install_selfsigned([domain], False) yunohost.certificate._certificate_install_selfsigned([domain], False)
@ -281,6 +276,25 @@ def get_public_ip(protocol=4):
raise MoulinetteError(errno.ENETUNREACH, raise MoulinetteError(errno.ENETUNREACH,
m18n.n('no_internet_connection')) m18n.n('no_internet_connection'))
def get_public_ips():
"""
Retrieve the public IPv4 and v6 from ip. and ip6.yunohost.org
Returns a 2-tuple (ipv4, ipv6). ipv4 or ipv6 can be None if they were not
found.
"""
try:
ipv4 = get_public_ip()
except:
ipv4 = None
try:
ipv6 = get_public_ip(6)
except:
ipv6 = None
return (ipv4, ipv6)
def _get_maindomain(): def _get_maindomain():
with open('/etc/yunohost/current_host', 'r') as f: with open('/etc/yunohost/current_host', 'r') as f:

View file

@ -36,45 +36,82 @@ import subprocess
from moulinette import m18n from moulinette import m18n
from moulinette.core import MoulinetteError from moulinette.core import MoulinetteError
from moulinette.utils.log import getActionLogger from moulinette.utils.log import getActionLogger
from moulinette.utils.filesystem import read_file, write_to_file, rm
from moulinette.utils.network import download_json
from yunohost.domain import get_public_ip, _get_maindomain, _build_dns_conf from yunohost.domain import get_public_ips, _get_maindomain, _build_dns_conf
logger = getActionLogger('yunohost.dyndns') logger = getActionLogger('yunohost.dyndns')
OLD_IPV4_FILE = '/etc/yunohost/dyndns/old_ip'
class IPRouteLine(object): OLD_IPV6_FILE = '/etc/yunohost/dyndns/old_ipv6'
""" Utility class to parse an ip route output line DYNDNS_ZONE = '/etc/yunohost/dyndns/zone'
The output of ip ro is variable and hard to parse completly, it would
require a real parser, not just a regexp, so do minimal parsing here...
>>> a = IPRouteLine('2001:: from :: via fe80::c23f:fe:1e:cafe dev eth0 src 2000:de:beef:ca:0:fe:1e:cafe metric 0')
>>> a.src_addr
"2000:de:beef:ca:0:fe:1e:cafe"
"""
regexp = re.compile(
r'(?P<unreachable>unreachable)?.*src\s+(?P<src_addr>[0-9a-f:]+).*')
def __init__(self, line):
self.m = self.regexp.match(line)
if not self.m:
raise ValueError("Not a valid ip route get line")
# make regexp group available as object attributes
for k, v in self.m.groupdict().items():
setattr(self, k, v)
RE_DYNDNS_PRIVATE_KEY_MD5 = re.compile( RE_DYNDNS_PRIVATE_KEY_MD5 = re.compile(
r'.*/K(?P<domain>[^\s\+]+)\.\+157.+\.private$' r'.*/K(?P<domain>[^\s\+]+)\.\+157.+\.private$'
) )
RE_DYNDNS_PRIVATE_KEY_SHA512 = re.compile( RE_DYNDNS_PRIVATE_KEY_SHA512 = re.compile(
r'.*/K(?P<domain>[^\s\+]+)\.\+165.+\.private$' r'.*/K(?P<domain>[^\s\+]+)\.\+165.+\.private$'
) )
def _dyndns_provides(provider, domain):
"""
Checks if a provider provide/manage a given domain.
Keyword arguments:
provider -- The url of the provider, e.g. "dyndns.yunohost.org"
domain -- The full domain that you'd like.. e.g. "foo.nohost.me"
Returns:
True if the provider provide/manages the domain. False otherwise.
"""
logger.debug("Checking if %s is managed by %s ..." % (domain, provider))
try:
# Dyndomains will be a list of domains supported by the provider
# e.g. [ "nohost.me", "noho.st" ]
dyndomains = download_json('https://%s/domains' % provider, timeout=30)
except MoulinetteError as e:
logger.error(str(e))
raise MoulinetteError(errno.EIO,
m18n.n('dyndns_could_not_check_provide',
domain=domain, provider=provider))
# Extract 'dyndomain' from 'domain', e.g. 'nohost.me' from 'foo.nohost.me'
dyndomain = '.'.join(domain.split('.')[1:])
return dyndomain in dyndomains
def _dyndns_available(provider, domain):
"""
Checks if a domain is available from a given provider.
Keyword arguments:
provider -- The url of the provider, e.g. "dyndns.yunohost.org"
domain -- The full domain that you'd like.. e.g. "foo.nohost.me"
Returns:
True if the domain is avaible, False otherwise.
"""
logger.debug("Checking if domain %s is available on %s ..."
% (domain, provider))
try:
r = download_json('https://%s/test/%s' % (provider, domain),
expected_status_code=None)
except MoulinetteError as e:
logger.error(str(e))
raise MoulinetteError(errno.EIO,
m18n.n('dyndns_could_not_check_available',
domain=domain, provider=provider))
return r == u"Domain %s is available" % domain
def dyndns_subscribe(subscribe_host="dyndns.yunohost.org", domain=None, key=None): def dyndns_subscribe(subscribe_host="dyndns.yunohost.org", domain=None, key=None):
""" """
Subscribe to a DynDNS service Subscribe to a DynDNS service
@ -88,12 +125,16 @@ def dyndns_subscribe(subscribe_host="dyndns.yunohost.org", domain=None, key=None
if domain is None: if domain is None:
domain = _get_maindomain() domain = _get_maindomain()
# Verify if domain is provided by subscribe_host
if not _dyndns_provides(subscribe_host, domain):
raise MoulinetteError(errno.ENOENT,
m18n.n('dyndns_domain_not_provided',
domain=domain, provider=subscribe_host))
# Verify if domain is available # Verify if domain is available
try: if not _dyndns_available(subscribe_host, domain):
if requests.get('https://%s/test/%s' % (subscribe_host, domain), timeout=30).status_code != 200: raise MoulinetteError(errno.ENOENT,
raise MoulinetteError(errno.EEXIST, m18n.n('dyndns_unavailable')) m18n.n('dyndns_unavailable', domain=domain))
except requests.ConnectionError:
raise MoulinetteError(errno.ENETUNREACH, m18n.n('no_internet_connection'))
if key is None: if key is None:
if len(glob.glob('/etc/yunohost/dyndns/*.key')) == 0: if len(glob.glob('/etc/yunohost/dyndns/*.key')) == 0:
@ -141,75 +182,40 @@ def dyndns_update(dyn_host="dyndns.yunohost.org", domain=None, key=None,
ipv6 -- IPv6 address to send ipv6 -- IPv6 address to send
""" """
# IPv4 # Get old ipv4/v6
old_ipv4, old_ipv6 = (None, None) # (default values)
if os.path.isfile(OLD_IPV4_FILE):
old_ipv4 = read_file(OLD_IPV4_FILE).rstrip()
if os.path.isfile(OLD_IPV6_FILE):
old_ipv6 = read_file(OLD_IPV6_FILE).rstrip()
# Get current IPv4 and IPv6
(ipv4_, ipv6_) = get_public_ips()
if ipv4 is None: if ipv4 is None:
ipv4 = get_public_ip() ipv4 = ipv4_
try:
with open('/etc/yunohost/dyndns/old_ip', 'r') as f:
old_ip = f.readline().rstrip()
except IOError:
old_ip = '0.0.0.0'
# IPv6
if ipv6 is None:
try:
ip_route_out = subprocess.check_output(
['ip', 'route', 'get', '2000::']).split('\n')
if len(ip_route_out) > 0:
route = IPRouteLine(ip_route_out[0])
if not route.unreachable:
ipv6 = route.src_addr
except (OSError, ValueError) as e:
# Unlikely case "ip route" does not return status 0
# or produces unexpected output
raise MoulinetteError(errno.EBADMSG,
"ip route cmd error : {}".format(e))
if ipv6 is None: if ipv6 is None:
logger.info(m18n.n('no_ipv6_connectivity')) ipv6 = ipv6_
try: logger.debug("Old IPv4/v6 are (%s, %s)" % (old_ipv4, old_ipv6))
with open('/etc/yunohost/dyndns/old_ipv6', 'r') as f: logger.debug("Requested IPv4/v6 are (%s, %s)" % (ipv4, ipv6))
old_ipv6 = f.readline().rstrip()
except IOError:
old_ipv6 = '0000:0000:0000:0000:0000:0000:0000:0000'
# no need to update # no need to update
if old_ip == ipv4 and old_ipv6 == ipv6: if old_ipv4 == ipv4 and old_ipv6 == ipv6:
logger.info("No updated needed.")
return return
else:
logger.info("Updated needed, going on...")
# If domain is not given, try to guess it from keys available...
if domain is None: if domain is None:
# Retrieve the first registered domain (domain, key) = _guess_current_dyndns_domain(dyn_host)
for path in glob.iglob('/etc/yunohost/dyndns/K*.private'): # If key is not given, pick the first file we find with the domain given
match = RE_DYNDNS_PRIVATE_KEY_SHA512.match(path) elif key is None:
if not match:
match = RE_DYNDNS_PRIVATE_KEY_MD5.match(path)
if not match:
continue
_domain = match.group('domain')
try:
# Check if domain is registered
request_url = 'https://{0}/test/{1}'.format(dyn_host, _domain)
if requests.get(request_url, timeout=30).status_code == 200:
continue
except requests.ConnectionError:
raise MoulinetteError(errno.ENETUNREACH,
m18n.n('no_internet_connection'))
except requests.exceptions.Timeout:
logger.warning("Correction timed out on {}, skip it".format(
request_url))
domain = _domain
key = path
break
if not domain:
raise MoulinetteError(errno.EINVAL,
m18n.n('dyndns_no_domain_registered'))
if key is None:
keys = glob.glob('/etc/yunohost/dyndns/K{0}.+*.private'.format(domain)) keys = glob.glob('/etc/yunohost/dyndns/K{0}.+*.private'.format(domain))
if not keys: if not keys:
@ -221,9 +227,12 @@ def dyndns_update(dyn_host="dyndns.yunohost.org", domain=None, key=None,
if "+157" in key: if "+157" in key:
key = _migrate_from_md5_tsig_to_sha512_tsig(key, domain, dyn_host) key = _migrate_from_md5_tsig_to_sha512_tsig(key, domain, dyn_host)
# Extract 'host', e.g. 'nohost.me' from 'foo.nohost.me'
host = domain.split('.')[1:] host = domain.split('.')[1:]
host = '.'.join(host) host = '.'.join(host)
logger.debug("Building zone update file ...")
lines = [ lines = [
'server %s' % dyn_host, 'server %s' % dyn_host,
'zone %s' % host, 'zone %s' % host,
@ -260,21 +269,27 @@ def dyndns_update(dyn_host="dyndns.yunohost.org", domain=None, key=None,
'send' 'send'
] ]
with open('/etc/yunohost/dyndns/zone', 'w') as zone: # Write the actions to do to update to a file, to be able to pass it
zone.write('\n'.join(lines)) # to nsupdate as argument
write_to_file(DYNDNS_ZONE, '\n'.join(lines))
if os.system('/usr/bin/nsupdate -k %s /etc/yunohost/dyndns/zone' % key) != 0: logger.info("Now pushing new conf to DynDNS host...")
os.system('rm -f /etc/yunohost/dyndns/old_ip')
os.system('rm -f /etc/yunohost/dyndns/old_ipv6') try:
command = ["/usr/bin/nsupdate", "-k", key, DYNDNS_ZONE]
subprocess.check_call(command)
except subprocess.CalledProcessError:
rm(OLD_IPV4_FILE, force=True) # Remove file (ignore if non-existent)
rm(OLD_IPV6_FILE, force=True) # Remove file (ignore if non-existent)
raise MoulinetteError(errno.EPERM, raise MoulinetteError(errno.EPERM,
m18n.n('dyndns_ip_update_failed')) m18n.n('dyndns_ip_update_failed'))
logger.success(m18n.n('dyndns_ip_updated')) logger.success(m18n.n('dyndns_ip_updated'))
with open('/etc/yunohost/dyndns/old_ip', 'w') as f:
f.write(ipv4) if ipv4 is not None:
write_to_file(OLD_IPV4_FILE, ipv4)
if ipv6 is not None: if ipv6 is not None:
with open('/etc/yunohost/dyndns/old_ipv6', 'w') as f: write_to_file(OLD_IPV6_FILE, ipv6)
f.write(ipv6)
def _migrate_from_md5_tsig_to_sha512_tsig(private_key_path, domain, dyn_host): def _migrate_from_md5_tsig_to_sha512_tsig(private_key_path, domain, dyn_host):
@ -356,3 +371,32 @@ def dyndns_removecron():
raise MoulinetteError(errno.EIO, m18n.n('dyndns_cron_remove_failed')) raise MoulinetteError(errno.EIO, m18n.n('dyndns_cron_remove_failed'))
logger.success(m18n.n('dyndns_cron_removed')) logger.success(m18n.n('dyndns_cron_removed'))
def _guess_current_dyndns_domain(dyn_host):
"""
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...)
"""
# Retrieve the first registered domain
for path in glob.iglob('/etc/yunohost/dyndns/K*.private'):
match = RE_DYNDNS_PRIVATE_KEY_MD5.match(path)
if not match:
match = RE_DYNDNS_PRIVATE_KEY_SHA512.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 _dyndns_available(dyn_host, _domain):
continue
else:
return (_domain, path)
raise MoulinetteError(errno.EINVAL,
m18n.n('dyndns_no_domain_registered'))

View file

@ -39,10 +39,10 @@ from moulinette.utils import log, filesystem
from yunohost.hook import hook_callback from yunohost.hook import hook_callback
BASE_CONF_PATH = '/home/yunohost.conf' BASE_CONF_PATH = '/home/yunohost.conf'
BACKUP_CONF_DIR = os.path.join(BASE_CONF_PATH, 'backup') BACKUP_CONF_DIR = os.path.join(BASE_CONF_PATH, 'backup')
PENDING_CONF_DIR = os.path.join(BASE_CONF_PATH, 'pending') PENDING_CONF_DIR = os.path.join(BASE_CONF_PATH, 'pending')
MOULINETTE_LOCK = "/var/run/moulinette_yunohost.lock"
logger = log.getActionLogger('yunohost.service') logger = log.getActionLogger('yunohost.service')
@ -493,7 +493,8 @@ def _run_service_command(action, service):
service -- Service name service -- Service name
""" """
if service not in _get_services().keys(): services = _get_services()
if service not in services.keys():
raise MoulinetteError(errno.EINVAL, m18n.n('service_unknown', service=service)) raise MoulinetteError(errno.EINVAL, m18n.n('service_unknown', service=service))
cmd = None cmd = None
@ -505,8 +506,23 @@ def _run_service_command(action, service):
else: else:
raise ValueError("Unknown action '%s'" % action) raise ValueError("Unknown action '%s'" % action)
need_lock = (services[service].get('need_lock') or False) \
and action in ['start', 'stop', 'restart', 'reload']
try: try:
ret = subprocess.check_output(cmd.split(), stderr=subprocess.STDOUT) # Launch the command
logger.debug("Running '%s'" % cmd)
p = subprocess.Popen(cmd.split(), stderr=subprocess.STDOUT)
# If this command needs a lock (because the service uses yunohost
# commands inside), find the PID and add a lock for it
if need_lock:
PID = _give_lock(action, service, p)
# Wait for the command to complete
p.communicate()
# Remove the lock if one was given
if need_lock and PID != 0:
_remove_lock(PID)
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
# TODO: Log output? # TODO: Log output?
logger.warning(m18n.n('service_cmd_exec_failed', command=' '.join(e.cmd))) logger.warning(m18n.n('service_cmd_exec_failed', command=' '.join(e.cmd)))
@ -514,6 +530,41 @@ def _run_service_command(action, service):
return True return True
def _give_lock(action, service, p):
# Depending of the action, systemctl calls the PID differently :/
if action == "start" or action == "restart":
systemctl_PID_name = "MainPID"
else:
systemctl_PID_name = "ControlPID"
cmd_get_son_PID ="systemctl show %s -p %s" % (service, systemctl_PID_name)
son_PID = 0
# As long as we did not found the PID and that the command is still running
while son_PID == 0 and p.poll() == None:
# Call systemctl to get the PID
# Output of the command is e.g. ControlPID=1234
son_PID = subprocess.check_output(cmd_get_son_PID.split()) \
.strip().split("=")[1]
son_PID = int(son_PID)
time.sleep(1)
# If we found a PID
if son_PID != 0:
# Append the PID to the lock file
logger.debug("Giving a lock to PID %s for service %s !"
% (str(son_PID), service))
filesystem.append_to_file(MOULINETTE_LOCK, "\n%s" % str(son_PID))
return son_PID
def _remove_lock(PID_to_remove):
PIDs = filesystem.read_file(MOULINETTE_LOCK).split("\n")
PIDs_to_keep = [ PID for PID in PIDs if int(PID) != PID_to_remove ]
filesystem.write_to_file(MOULINETTE_LOCK, '\n'.join(PIDs_to_keep))
def _get_services(): def _get_services():
""" """
Get a dict of managed services with their parameters Get a dict of managed services with their parameters

View file

@ -45,7 +45,7 @@ from moulinette.utils.log import getActionLogger
from moulinette.utils.filesystem import read_json, write_to_json from moulinette.utils.filesystem import read_json, write_to_json
from yunohost.app import app_fetchlist, app_info, app_upgrade, app_ssowatconf, app_list, _install_appslist_fetch_cron from yunohost.app import app_fetchlist, app_info, app_upgrade, app_ssowatconf, app_list, _install_appslist_fetch_cron
from yunohost.domain import domain_add, domain_list, get_public_ip, _get_maindomain, _set_maindomain from yunohost.domain import domain_add, domain_list, get_public_ip, _get_maindomain, _set_maindomain
from yunohost.dyndns import dyndns_subscribe from yunohost.dyndns import _dyndns_available, _dyndns_provides
from yunohost.firewall import firewall_upnp from yunohost.firewall import firewall_upnp
from yunohost.service import service_status, service_regen_conf, service_log from yunohost.service import service_status, service_regen_conf, service_log
from yunohost.monitor import monitor_disk, monitor_system from yunohost.monitor import monitor_disk, monitor_system
@ -253,27 +253,39 @@ def tools_postinstall(domain, password, ignore_dyndns=False):
password -- YunoHost admin password password -- YunoHost admin password
""" """
dyndns = not ignore_dyndns dyndns_provider = "dyndns.yunohost.org"
# Do some checks at first # Do some checks at first
if os.path.isfile('/etc/yunohost/installed'): if os.path.isfile('/etc/yunohost/installed'):
raise MoulinetteError(errno.EPERM, raise MoulinetteError(errno.EPERM,
m18n.n('yunohost_already_installed')) m18n.n('yunohost_already_installed'))
if len(domain.split('.')) >= 3 and not ignore_dyndns:
try:
r = requests.get('https://dyndns.yunohost.org/domains')
except requests.ConnectionError:
pass
else:
dyndomains = json.loads(r.text)
dyndomain = '.'.join(domain.split('.')[1:])
if dyndomain in dyndomains: if not ignore_dyndns:
if requests.get('https://dyndns.yunohost.org/test/%s' % domain).status_code == 200: # Check if yunohost dyndns can handle the given domain
# (i.e. is it a .nohost.me ? a .noho.st ?)
try:
is_nohostme_or_nohost = _dyndns_provides(dyndns_provider, domain)
# If an exception is thrown, most likely we don't have internet
# connectivity or something. Assume that this domain isn't manageable
# and inform the user that we could not contact the dyndns host server.
except:
logger.warning(m18n.n('dyndns_provider_unreachable',
provider=dyndns_provider))
is_nohostme_or_nohost = False
# If this is a nohost.me/noho.st, actually check for availability
if is_nohostme_or_nohost:
# (Except if the user explicitly said he/she doesn't care about dyndns)
if ignore_dyndns:
dyndns = False
# Check if the domain is available...
elif _dyndns_available(dyndns_provider, domain):
dyndns = True dyndns = True
# If not, abort the postinstall
else: else:
raise MoulinetteError(errno.EEXIST, raise MoulinetteError(errno.EEXIST,
m18n.n('dyndns_unavailable')) m18n.n('dyndns_unavailable',
domain=domain))
else: else:
dyndns = False dyndns = False
else: else:

View file

@ -82,7 +82,7 @@ def get_crt(account_key, csr, acme_dir, log=LOGGER, CA=DEFAULT_CA):
if proc.returncode != 0: if proc.returncode != 0:
raise IOError("Error loading {0}: {1}".format(csr, err)) raise IOError("Error loading {0}: {1}".format(csr, err))
domains = set([]) domains = set([])
common_name = re.search(r"Subject:.*? CN=([^\s,;/]+)", out.decode('utf8')) common_name = re.search(r"Subject:.*? CN\s?=\s?([^\s,;/]+)", out.decode('utf8'))
if common_name is not None: if common_name is not None:
domains.add(common_name.group(1)) domains.add(common_name.group(1))
subject_alt_names = re.search(r"X509v3 Subject Alternative Name: \n +([^\n]+)\n", out.decode('utf8'), re.MULTILINE|re.DOTALL) subject_alt_names = re.search(r"X509v3 Subject Alternative Name: \n +([^\n]+)\n", out.decode('utf8'), re.MULTILINE|re.DOTALL)
@ -95,7 +95,7 @@ def get_crt(account_key, csr, acme_dir, log=LOGGER, CA=DEFAULT_CA):
log.info("Registering account...") log.info("Registering account...")
code, result = _send_signed_request(CA + "/acme/new-reg", { code, result = _send_signed_request(CA + "/acme/new-reg", {
"resource": "new-reg", "resource": "new-reg",
"agreement": "https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf", "agreement": json.loads(urlopen(CA + "/directory").read().decode('utf8'))['meta']['terms-of-service'],
}) })
if code == 201: if code == 201:
log.info("Registered!") log.info("Registered!")