diff --git a/bin/yunoprompt b/bin/yunoprompt index de05dd6fa..2705dbcdc 100755 --- a/bin/yunoprompt +++ b/bin/yunoprompt @@ -56,7 +56,7 @@ else echo "$LOGO_AND_FINGERPRINTS" echo -e "\e[m Post-installation \e[0m" echo "Congratulations! YunoHost has been successfully installed.\nTwo more steps are required to activate the services of your server." - read -p "Proceed to post-installation? (y/n) " -n 1 + read -p "Proceed to post-installation? (y/n)\nAlternatively, you can proceed the post-installation on https://${ip}" -n 1 RESULT=1 while [ $RESULT -gt 0 ]; do if [[ $REPLY =~ ^[Nn]$ ]]; then diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 222921747..ba5f1d505 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -105,7 +105,7 @@ user: ask: ask_lastname required: True pattern: &pattern_lastname - - !!str ^([^\W\d_]{2,30}[ ,.']{0,3})+$ + - !!str ^([^\W\d_]{2,30}[ ,.'-]{0,3})+$ - "pattern_lastname" -m: full: --mail @@ -125,6 +125,7 @@ user: pattern: &pattern_password - !!str ^.{3,}$ - "pattern_password" + comment: good_practices_about_user_password -q: full: --mailbox-quota help: Mailbox size quota @@ -780,18 +781,18 @@ app: ### app_action_list() list: action_help: List app actions - api: GET /apps//actions + api: GET /apps//actions arguments: - app_id: - help: app id + app: + help: App name ### app_action_run() run: action_help: Run app action - api: PUT /apps//actions/ + api: PUT /apps//actions/ arguments: - app_id: - help: app id + app: + help: App name action: help: action id -a: @@ -805,18 +806,18 @@ app: ### app_config_show_panel() show-panel: action_help: show config panel for the application - api: GET /apps//config-panel + api: GET /apps//config-panel arguments: - app_id: - help: App ID + app: + help: App name ### app_config_apply() apply: action_help: apply the new configuration - api: POST /apps//config + api: POST /apps//config arguments: - app_id: - help: App ID + app: + help: App name -a: full: --args help: Serialized arguments for new configuration (i.e. "domain=domain.tld&path=/path") @@ -1136,6 +1137,13 @@ service: full: --runlevel help: Runlevel priority of the service type: int + -n: + full: --need_lock + help: Use this option to prevent deadlocks if the service does invoke yunohost commands. + action: store_true + -d: + full: --description + help: Description of the service ### service_remove() remove: @@ -1449,6 +1457,7 @@ tools: password: ask_new_admin_password pattern: *pattern_password required: True + comment: good_practices_about_admin_password ### tools_maindomain() maindomain: @@ -1486,9 +1495,13 @@ tools: password: ask_new_admin_password pattern: *pattern_password required: True + comment: good_practices_about_admin_password --ignore-dyndns: help: Do not subscribe domain to a DynDNS service action: store_true + --force-password: + help: Use this if you really want to set a weak password + action: store_true ### tools_update() update: diff --git a/data/helpers.d/backend b/data/helpers.d/backend index 8dce2df06..94e26350c 100644 --- a/data/helpers.d/backend +++ b/data/helpers.d/backend @@ -1,8 +1,9 @@ # Use logrotate to manage the logfile # -# usage: ynh_use_logrotate [logfile] [--non-append] +# usage: ynh_use_logrotate [logfile] [--non-append|--append] [specific_user/specific_group] # | arg: logfile - absolute path of logfile # | arg: --non-append - (Option) Replace the config file instead of appending this new config. +# | arg: specific_user : run logrotate as the specified user and group. If not specified logrotate is runned as root. # # 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. @@ -13,6 +14,7 @@ # Unless you use the option --non-append ynh_use_logrotate () { local customtee="tee -a" + local user_group="${3:-}" if [ $# -gt 0 ] && [ "$1" == "--non-append" ]; then customtee="tee" # Destroy this argument for the next command. @@ -29,6 +31,12 @@ ynh_use_logrotate () { else local logfile="/var/log/${app}/*.log" # Without argument, use a defaut directory in /var/log fi + local su_directive="" + if [[ -n $user_group ]]; then + su_directive=" # Run logorotate as specific user - group + su ${user_group%/*} ${user_group#*/}" + fi + cat > ./${app}-logrotate << EOF # Build a config file for logrotate $logfile { # Rotate if the logfile exceeds 100Mo @@ -47,6 +55,7 @@ $logfile { notifempty # Keep old logs in the same dir noolddir + $su_directive } EOF sudo mkdir -p $(dirname "$logfile") # Create the log directory, if not exist diff --git a/data/helpers.d/filesystem b/data/helpers.d/filesystem index d4146ad8f..c07de2ece 100644 --- a/data/helpers.d/filesystem +++ b/data/helpers.d/filesystem @@ -299,6 +299,23 @@ ynh_backup_if_checksum_is_different () { fi } +# Delete a file checksum from the app settings +# +# $app should be defined when calling this helper +# +# usage: ynh_remove_file_checksum file +# | arg: -f, --file= - The file for which the checksum will be deleted +ynh_delete_file_checksum () { + # Declare an array to define the options of this helper. + declare -Ar args_array=( [f]=file= ) + local file + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + + local checksum_setting_name=checksum_${file//[\/ ]/_} # Replace all '/' and ' ' by '_' + ynh_app_setting_delete $app $checksum_setting_name +} + # Remove a file or a directory securely # # usage: ynh_secure_remove path_to_remove diff --git a/data/helpers.d/getopts b/data/helpers.d/getopts new file mode 100644 index 000000000..1cc66da8a --- /dev/null +++ b/data/helpers.d/getopts @@ -0,0 +1,194 @@ +#!/bin/bash + +# Internal helper design to allow helpers to use getopts to manage their arguments +# +# [internal] +# +# example: function my_helper() +# { +# declare -Ar args_array=( [a]=arg1= [b]=arg2= [c]=arg3 ) +# local arg1 +# local arg2 +# local arg3 +# ynh_handle_getopts_args "$@" +# +# [...] +# } +# my_helper --arg1 "val1" -b val2 -c +# +# usage: ynh_handle_getopts_args "$@" +# | arg: $@ - Simply "$@" to tranfert all the positionnal arguments to the function +# +# This helper need an array, named "args_array" with all the arguments used by the helper +# that want to use ynh_handle_getopts_args +# Be carreful, this array has to be an associative array, as the following example: +# declare -Ar args_array=( [a]=arg1 [b]=arg2= [c]=arg3 ) +# Let's explain this array: +# a, b and c are short options, -a, -b and -c +# arg1, arg2 and arg3 are the long options associated to the previous short ones. --arg1, --arg2 and --arg3 +# For each option, a short and long version has to be defined. +# Let's see something more significant +# declare -Ar args_array=( [u]=user [f]=finalpath= [d]=database ) +# +# NB: Because we're using 'declare' without -g, the array will be declared as a local variable. +# +# Please keep in mind that the long option will be used as a variable to store the values for this option. +# For the previous example, that means that $finalpath will be fill with the value given as argument for this option. +# +# Also, in the previous example, finalpath has a '=' at the end. That means this option need a value. +# So, the helper has to be call with --finalpath /final/path, --finalpath=/final/path or -f /final/path, the variable $finalpath will get the value /final/path +# If there's many values for an option, -f /final /path, the value will be separated by a ';' $finalpath=/final;/path +# For an option without value, like --user in the example, the helper can be called only with --user or -u. $user will then get the value 1. +# +# To keep a retrocompatibility, a package can still call a helper, using getopts, with positional arguments. +# The "legacy mode" will manage the positional arguments and fill the variable in the same order than they are given in $args_array. +# e.g. for `my_helper "val1" val2`, arg1 will be filled with val1, and arg2 with val2. +ynh_handle_getopts_args () { + # Manage arguments only if there's some provided + set +x + if [ $# -ne 0 ] + then + # Store arguments in an array to keep each argument separated + local arguments=("$@") + + # For each option in the array, reduce to short options for getopts (e.g. for [u]=user, --user will be -u) + # And built parameters string for getopts + # ${!args_array[@]} is the list of all keys in the array (A key is 'u' in [u]=user, user is a value) + local getopts_parameters="" + local key="" + for key in "${!args_array[@]}" + do + # Concatenate each keys of the array to build the string of arguments for getopts + # Will looks like 'abcd' for -a -b -c -d + # If the value of a key finish by =, it's an option with additionnal values. (e.g. --user bob or -u bob) + # Check the last character of the value associate to the key + if [ "${args_array[$key]: -1}" = "=" ] + then + # For an option with additionnal values, add a ':' after the letter for getopts. + getopts_parameters="${getopts_parameters}${key}:" + else + getopts_parameters="${getopts_parameters}${key}" + fi + # Check each argument given to the function + local arg="" + # ${#arguments[@]} is the size of the array + for arg in `seq 0 $(( ${#arguments[@]} - 1 ))` + do + # And replace long option (value of the key) by the short option, the key itself + # (e.g. for [u]=user, --user will be -u) + # Replace long option with = + arguments[arg]="${arguments[arg]//--${args_array[$key]}/-${key} }" + # And long option without = + arguments[arg]="${arguments[arg]//--${args_array[$key]%=}/-${key}}" + done + done + + # Read and parse all the arguments + # Use a function here, to use standart arguments $@ and be able to use shift. + parse_arg () { + # Read all arguments, until no arguments are left + while [ $# -ne 0 ] + do + # Initialize the index of getopts + OPTIND=1 + # Parse with getopts only if the argument begin by -, that means the argument is an option + # getopts will fill $parameter with the letter of the option it has read. + local parameter="" + getopts ":$getopts_parameters" parameter || true + + if [ "$parameter" = "?" ] + then + ynh_die "Invalid argument: -${OPTARG:-}" + elif [ "$parameter" = ":" ] + then + ynh_die "-$OPTARG parameter requires an argument." + else + local shift_value=1 + # Use the long option, corresponding to the short option read by getopts, as a variable + # (e.g. for [u]=user, 'user' will be used as a variable) + # Also, remove '=' at the end of the long option + # The variable name will be stored in 'option_var' + local option_var="${args_array[$parameter]%=}" + # If this option doesn't take values + # if there's a '=' at the end of the long option name, this option takes values + if [ "${args_array[$parameter]: -1}" != "=" ] + then + # 'eval ${option_var}' will use the content of 'option_var' + eval ${option_var}=1 + else + # Read all other arguments to find multiple value for this option. + # Load args in a array + local all_args=("$@") + + # If the first argument is longer than 2 characters, + # There's a value attached to the option, in the same array cell + if [ ${#all_args[0]} -gt 2 ]; then + # Remove the option and the space, so keep only the value itself. + all_args[0]="${all_args[0]#-${parameter} }" + # Reduce the value of shift, because the option has been removed manually + shift_value=$(( shift_value - 1 )) + fi + + # Then read the array value per value + for i in `seq 0 $(( ${#all_args[@]} - 1 ))` + do + # If this argument is an option, end here. + if [ "${all_args[$i]:0:1}" == "-" ] || [ -z "${all_args[$i]}" ] + then + # Ignore the first value of the array, which is the option itself + if [ "$i" -ne 0 ]; then + break + fi + else + # Declare the content of option_var as a variable. + eval ${option_var}="" + # Else, add this value to this option + # Each value will be separated by ';' + if [ -n "${!option_var}" ] + then + # If there's already another value for this option, add a ; before adding the new value + eval ${option_var}+="\;" + fi + eval ${option_var}+=\"${all_args[$i]}\" + shift_value=$(( shift_value + 1 )) + fi + done + fi + fi + + # Shift the parameter and its argument(s) + shift $shift_value + done + } + + # LEGACY MODE + # Check if there's getopts arguments + if [ "${arguments[0]:0:1}" != "-" ] + then + # If not, enter in legacy mode and manage the arguments as positionnal ones. + echo "! Helper used in legacy mode !" + for i in `seq 0 $(( ${#arguments[@]} -1 ))` + do + # Use getopts_parameters as a list of key of the array args_array + # Remove all ':' in getopts_parameters + getopts_parameters=${getopts_parameters//:} + # Get the key from getopts_parameters, by using the key according to the position of the argument. + key=${getopts_parameters:$i:1} + # Use the long option, corresponding to the key, as a variable + # (e.g. for [u]=user, 'user' will be used as a variable) + # Also, remove '=' at the end of the long option + # The variable name will be stored in 'option_var' + local option_var="${args_array[$key]%=}" + + # Store each value given as argument in the corresponding variable + # The values will be stored in the same order than $args_array + eval ${option_var}+=\"${arguments[$i]}\" + done + else + # END LEGACY MODE + # Call parse_arg and pass the modified list of args as an array of arguments. + parse_arg "${arguments[@]}" + fi + fi + set -x +} diff --git a/data/helpers.d/print b/data/helpers.d/print index d35c3e929..93d402e64 100644 --- a/data/helpers.d/print +++ b/data/helpers.d/print @@ -8,12 +8,11 @@ ynh_die() { # Display a message in the 'INFO' logging category # # usage: ynh_info "Some message" -ynh_info() -{ - echo "$1" >>"$YNH_STDINFO" +ynh_print_info() { + echo "$1" >> "$YNH_STDINFO" } -# Ignore the yunohost-cli log to prevent errors with conditionals commands +# Ignore the yunohost-cli log to prevent errors with conditional commands # # [internal] # @@ -29,3 +28,99 @@ ynh_no_log() { sudo mv ${ynh_cli_log}-move ${ynh_cli_log} return $? } + +# Main printer, just in case in the future we have to change anything about that. +# +# [internal] +# +ynh_print_log () { + echo -e "${1}" +} + +# Print a warning on stderr +# +# usage: ynh_print_warn "Text to print" +# | arg: text - The text to print +ynh_print_warn () { + ynh_print_log "\e[93m\e[1m[WARN]\e[0m ${1}" >&2 +} + +# Print an error on stderr +# +# usage: ynh_print_err "Text to print" +# | arg: text - The text to print +ynh_print_err () { + ynh_print_log "\e[91m\e[1m[ERR]\e[0m ${1}" >&2 +} + +# Execute a command and print the result as an error +# +# usage: ynh_exec_err command to execute +# usage: ynh_exec_err "command to execute | following command" +# In case of use of pipes, you have to use double quotes. Otherwise, this helper will be executed with the first command, then be sent to the next pipe. +# +# | arg: command - command to execute +ynh_exec_err () { + ynh_print_err "$(eval $@)" +} + +# Execute a command and print the result as a warning +# +# usage: ynh_exec_warn command to execute +# usage: ynh_exec_warn "command to execute | following command" +# In case of use of pipes, you have to use double quotes. Otherwise, this helper will be executed with the first command, then be sent to the next pipe. +# +# | arg: command - command to execute +ynh_exec_warn () { + ynh_print_warn "$(eval $@)" +} + +# Execute a command and force the result to be printed on stdout +# +# usage: ynh_exec_warn_less command to execute +# usage: ynh_exec_warn_less "command to execute | following command" +# In case of use of pipes, you have to use double quotes. Otherwise, this helper will be executed with the first command, then be sent to the next pipe. +# +# | arg: command - command to execute +ynh_exec_warn_less () { + eval $@ 2>&1 +} + +# Execute a command and redirect stdout in /dev/null +# +# usage: ynh_exec_quiet command to execute +# usage: ynh_exec_quiet "command to execute | following command" +# In case of use of pipes, you have to use double quotes. Otherwise, this helper will be executed with the first command, then be sent to the next pipe. +# +# | arg: command - command to execute +ynh_exec_quiet () { + eval $@ > /dev/null +} + +# Execute a command and redirect stdout and stderr in /dev/null +# +# usage: ynh_exec_fully_quiet command to execute +# usage: ynh_exec_fully_quiet "command to execute | following command" +# In case of use of pipes, you have to use double quotes. Otherwise, this helper will be executed with the first command, then be sent to the next pipe. +# +# | arg: command - command to execute +ynh_exec_fully_quiet () { + eval $@ > /dev/null 2>&1 +} + +# Remove any logs for all the following commands. +# +# usage: ynh_print_OFF +# WARNING: You should be careful with this helper, and never forget to use ynh_print_ON as soon as possible to restore the logging. +ynh_print_OFF () { + set +x +} + +# Restore the logging after ynh_print_OFF +# +# usage: ynh_print_ON +ynh_print_ON () { + set -x + # Print an echo only for the log, to be able to know that ynh_print_ON has been called. + echo ynh_print_ON > /dev/null +} diff --git a/data/helpers.d/utils b/data/helpers.d/utils index 595da3c2d..eef9f2a8e 100644 --- a/data/helpers.d/utils +++ b/data/helpers.d/utils @@ -173,7 +173,7 @@ ynh_setup_source () { then # Use the local source file if it is present cp $local_src $src_filename else # If not, download the source - wget -nv -O $src_filename $src_url + local out=`wget -nv -O $src_filename $src_url 2>&1` || ynh_print_err $out fi # Check the control sum @@ -253,6 +253,9 @@ ynh_local_curl () { # Add --data arg and remove the last character, which is an unecessary '&' POST_data="--data ${POST_data::-1}" fi + + # Wait untils nginx has fully reloaded (avoid curl fail with http2) + sleep 2 # Curl the URL curl --silent --show-error -kL -H "Host: $domain" --resolve $domain:443:127.0.0.1 $POST_data "$full_page_url" diff --git a/data/hooks/backup/17-data_home b/data/hooks/backup/17-data_home index 9e5aba5a5..f7a797b6b 100755 --- a/data/hooks/backup/17-data_home +++ b/data/hooks/backup/17-data_home @@ -12,6 +12,8 @@ backup_dir="${1}/data/home" # Backup user home for f in $(find /home/* -type d -prune | awk -F/ '{print $NF}'); do if [[ ! "$f" =~ ^yunohost|lost\+found ]]; then - ynh_backup "/home/$f" "${backup_dir}/$f" 1 + if [ ! -e "/home/$f/.nobackup" ]; then + ynh_backup "/home/$f" "${backup_dir}/$f" 1 + fi fi done diff --git a/data/hooks/conf_regen/15-nginx b/data/hooks/conf_regen/15-nginx index 1aafcbfa2..461c10c0c 100755 --- a/data/hooks/conf_regen/15-nginx +++ b/data/hooks/conf_regen/15-nginx @@ -2,6 +2,8 @@ set -e +. /usr/share/yunohost/helpers.d/utils + do_init_regen() { if [[ $EUID -ne 0 ]]; then echo "You must be root to run this script" 1>&2 @@ -42,18 +44,18 @@ do_pre_regen() { mkdir -p "$mail_autoconfig_dir" # NGINX server configuration - cat server.tpl.conf \ - | sed "s/{{ domain }}/${domain}/g" \ - > "${nginx_conf_dir}/${domain}.conf" - - cat autoconfig.tpl.xml \ - | sed "s/{{ domain }}/${domain}/g" \ - > "${mail_autoconfig_dir}/config-v1.1.xml" + export domain + export domain_cert_ca=$(yunohost domain cert-status $domain --json \ + | jq ".certificates.\"$domain\".CA_type" \ + | tr -d '"') + ynh_render_template "server.tpl.conf" "${nginx_conf_dir}/${domain}.conf" + ynh_render_template "autoconfig.tpl.xml" "${mail_autoconfig_dir}/config-v1.1.xml" [[ $main_domain != $domain ]] \ && touch "${domain_conf_dir}/yunohost_local.conf" \ || cp yunohost_local.conf "${domain_conf_dir}/yunohost_local.conf" + done # remove old domain conf files diff --git a/data/other/password/100000-most-used.txt.gz b/data/other/password/100000-most-used.txt.gz new file mode 100644 index 000000000..43887119b Binary files /dev/null and b/data/other/password/100000-most-used.txt.gz differ diff --git a/data/templates/metronome/metronome.cfg.lua b/data/templates/metronome/metronome.cfg.lua index 012f427ef..0640ef9d5 100644 --- a/data/templates/metronome/metronome.cfg.lua +++ b/data/templates/metronome/metronome.cfg.lua @@ -9,16 +9,64 @@ -- A table is a list of values, except each value has a name. An -- example would be: -- --- ssl = { key = "keyfile.key", certificate = "certificate.crt" } +-- ssl = { key = "keyfile.key", certificate = "certificate.cert" } -- -- Tip: You can check that the syntax of this file is correct when you have finished -- by running: luac -p metronome.cfg.lua -- If there are any errors, it will let you know what and where they are, otherwise it -- will keep quiet. ----------- Server-wide settings ---------- --- Settings in this section apply to the whole server and are the default settings --- for any virtual hosts +-- Global settings go in this section + +-- This is the list of modules Metronome will load on startup. +-- It looks for mod_modulename.lua in the plugins folder, so make sure that exists too. + +modules_enabled = { + -- Generally required + "roster"; -- Allow users to have a roster. Recommended. + "saslauth"; -- Authentication for clients. Recommended if you want to log in. + "tls"; -- Add support for secure TLS on c2s/s2s connections + "disco"; -- Service discovery + + -- Not essential, but recommended + "private"; -- Private XML storage (for room bookmarks, etc.) + "vcard"; -- Allow users to set vCards + "pep"; -- Allows setting of mood, tune, etc. + "posix"; -- POSIX functionality, sends server to background, enables syslog, etc. + "bidi"; -- Enables Bidirectional Server-to-Server Streams. + + -- Nice to have + "version"; -- Replies to server version requests + "uptime"; -- Report how long server has been running + "time"; -- Let others know the time here on this server + "ping"; -- Replies to XMPP pings with pongs + "register"; -- Allow users to register on this server using a client and change passwords + "stream_management"; -- Allows clients and servers to use Stream Management + "stanza_optimizations"; -- Allows clients to use Client State Indication and SIFT + "message_carbons"; -- Allows clients to enable carbon copies of messages + "mam"; -- Enable server-side message archives using Message Archive Management + "push"; -- Enable Push Notifications via PubSub using XEP-0357 + "lastactivity"; -- Enables clients to know the last presence status of an user + "adhoc_cm"; -- Allow to set client certificates to login through SASL External via adhoc + "admin_adhoc"; -- administration adhoc commands + "bookmarks"; -- XEP-0048 Bookmarks synchronization between PEP and Private Storage + "sec_labels"; -- Allows to use a simplified version XEP-0258 Security Labels and related ACDFs. + "privacy"; -- Add privacy lists and simple blocking command support + + -- Other specific functionality + --"admin_telnet"; -- administration console, telnet to port 5582 + --"admin_web"; -- administration web interface + "bosh"; -- Enable support for BOSH clients, aka "XMPP over Bidirectional Streams over Synchronous HTTP" + --"compression"; -- Allow clients to enable Stream Compression + --"spim_block"; -- Require authorization via OOB form for messages from non-contacts and block unsollicited messages + --"gate_guard"; -- Enable config-based blacklisting and hit-based auto-banning features + --"incidents_handling"; -- Enable Incidents Handling support (can be administered via adhoc commands) + --"server_presence"; -- Enables Server Buddies extension support + --"service_directory"; -- Enables Service Directories extension support + --"public_service"; -- Enables Server vCard support for public services in directories and advertises in features + --"register_api"; -- Provides secure API for both Out-Of-Band and In-Band registration for E-Mail verification + "websocket"; -- Enable support for WebSocket clients, aka "XMPP over WebSockets" +}; -- Server PID pidfile = "/var/run/metronome/metronome.pid" @@ -33,156 +81,76 @@ http_interfaces = { "127.0.0.1", "::1" } -- Enable IPv6 use_ipv6 = true --- This is the list of modules Metronome will load on startup. --- It looks for mod_modulename.lua in the plugins folder, so make sure that exists too. -modules_enabled = { - - -- Generally required - "roster"; -- Allow users to have a roster. Recommended ;) - "saslauth"; -- Authentication for clients and servers. Recommended if you want to log in. - "tls"; -- Add support for secure TLS on c2s/s2s connections - "dialback"; -- s2s dialback support - "disco"; -- Service discovery - --"discoitems"; -- Service discovery items - --"extdisco"; -- External Service Discovery - - -- Not essential, but recommended - "private"; -- Private XML storage (for room bookmarks, etc.) - "vcard"; -- Allow users to set vCards - "privacy"; -- Support privacy lists - - -- These are commented by default as they have a performance impact - --"compression"; -- Stream compression (Debian: requires lua-zlib module to work) - - -- Nice to have - "version"; -- Replies to server version requests - "uptime"; -- Report how long server has been running - "time"; -- Let others know the time here on this server - "ping"; -- Replies to XMPP pings with pongs - "pep"; -- Enables users to publish their mood, activity, playing music and more - "message_carbons"; -- Allow clients to keep in sync with messages send on other resources - "register"; -- Allow users to register on this server using a client and change passwords - "adhoc"; -- Support for "ad-hoc commands" that can be executed with an XMPP client - - -- Admin interfaces - "admin_adhoc"; -- Allows administration via an XMPP client that supports ad-hoc commands - "admin_telnet"; -- Opens telnet console interface on localhost port 5582 - - -- HTTP modules - "bosh"; -- Enable BOSH clients, aka "Jabber over HTTP" - --"websockets"; -- Enable WebSocket clients - --"http_files"; -- Serve static files from a directory over HTTP - - -- Other specific functionality --- "bidi"; -- Bidirectional Streams for S2S connections --- "stream_management"; -- Stream Management support - --"groups"; -- Shared roster support - --"announce"; -- Send announcement to all online users - --"welcome"; -- Welcome users who register accounts - --"watchregistrations"; -- Alert admins of registrations - --"motd"; -- Send a message to users when they log in - "mam"; -- Nice archive management - --"legacyauth"; -- Legacy authentication. Only used by some old clients and bots. - "offline"; -- Store offline messages - "c2s"; -- Handle client connections - "s2s"; -- Handle server-to-server connections - - -- Debian: do not remove this module, or you lose syslog - -- support - "posix"; -- POSIX functionality, sends server to background, enables syslog, etc. -}; - -- Discovery items disco_items = { - { "muc.{{ main_domain }}" }, - { "pubsub.{{ main_domain }}" }, - { "vjud.{{ main_domain }}" } + { "muc.{{ main_domain }}" }, + { "pubsub.{{ main_domain }}" }, + { "upload.{{ main_domain }}" }, + { "vjud.{{ main_domain }}" } }; -- BOSH configuration (mod_bosh) -bosh_max_inactivity = 30 consider_bosh_secure = true cross_domain_bosh = true +-- WebSocket configuration (mod_websocket) +consider_websocket_secure = true +cross_domain_websocket = true + -- Disable account creation by default, for security allow_registration = false --- SSL/TLS configuration -ssl = { - options = { - "no_sslv2", - "no_sslv3", - "no_ticket", - "no_compression", - "cipher_server_preference" - }; -} - --- Force clients to use encrypted connections? This option will --- prevent clients from authenticating unless they are using encryption. -c2s_require_encryption = true - --- Force servers to use encrypted connections? This option will --- prevent servers from connecting unless they are using encryption. -s2s_require_encryption = true - --- Allow servers to use an unauthenticated encryption channel -s2s_allow_encryption = true - -allow_unencrypted_plain_auth = false; - -s2s_secure = true -s2s_secure_auth = false - ---anonymous_login = false - -- Use LDAP storage backend for all stores storage = "ldap" -- Logging configuration log = { - info = "/var/log/metronome/metronome.log"; -- Change 'info' to 'debug' for verbose logging - error = "/var/log/metronome/metronome.err"; - -- "*syslog"; -- Uncomment this for logging to syslog - -- "*console"; -- Log to the console, useful for debugging with daemonize=false + info = "/var/log/metronome/metronome.log"; -- Change 'info' to 'debug' for verbose logging + error = "/var/log/metronome/metronome.err"; + -- "*syslog"; -- Uncomment this for logging to syslog + -- "*console"; -- Log to the console, useful for debugging with daemonize=false } - ------ Components ------ -- You can specify components to add hosts that provide special services, -- like multi-user conferences, and transports. ---Set up a local BOSH service Component "localhost" "http" - modules_enabled = { "bosh" } + modules_enabled = { "bosh" } ---Set up a MUC (multi-user chat) room server Component "muc.{{ main_domain }}" "muc" - name = "{{ main_domain }} Chatrooms" + name = "{{ main_domain }} Chatrooms" - modules_enabled = { - "muc_limits"; - "muc_log"; - "muc_log_http"; + modules_enabled = { + "muc_limits"; + "muc_log"; + "muc_log_mam"; + "muc_log_http"; + "muc_vcard"; } - muc_event_rate = 0.5 - muc_burst_factor = 10 - - muc_log_http_config = { - url_base = "logs"; - theme = "metronome"; - } + muc_event_rate = 0.5 + muc_burst_factor = 10 ---Set up a PubSub server Component "pubsub.{{ main_domain }}" "pubsub" - name = "{{ main_domain }} Publish/Subscribe" + name = "{{ main_domain }} Publish/Subscribe" + + unrestricted_node_creation = true -- Anyone can create a PubSub node (from any server) + +---Set up a HTTP Upload service +Component "upload.{{ main_domain }}" "http_upload" + name = "{{ main_domain }} Sharing Service" + + http_file_size_limit = 6*1024*1024 + http_file_quota = 60*1024*1024 - unrestricted_node_creation = true -- Anyone can create a PubSub node (from any server) ---Set up a VJUD service Component "vjud.{{ main_domain }}" "vjud" - ud_disco_name = "{{ main_domain }} User Directory" + ud_disco_name = "{{ main_domain }} User Directory" ----------- Virtual hosts ----------- @@ -190,4 +158,3 @@ Component "vjud.{{ main_domain }}" "vjud" -- Settings under each VirtualHost entry apply *only* to that host. Include "conf.d/*.cfg.lua" - diff --git a/data/templates/nginx/plain/yunohost_admin.conf b/data/templates/nginx/plain/yunohost_admin.conf index 41065d2bc..3de66e3e6 100644 --- a/data/templates/nginx/plain/yunohost_admin.conf +++ b/data/templates/nginx/plain/yunohost_admin.conf @@ -73,6 +73,6 @@ server { access_by_lua_file /usr/share/ssowat/access.lua; } - include conf.d/yunohost_admin.conf.inc; - include conf.d/yunohost_api.conf.inc; + include /etc/nginx/conf.d/yunohost_admin.conf.inc; + include /etc/nginx/conf.d/yunohost_api.conf.inc; } diff --git a/data/templates/nginx/plain/yunohost_api.conf.inc b/data/templates/nginx/plain/yunohost_api.conf.inc index 35cd0090c..4d7887cc6 100644 --- a/data/templates/nginx/plain/yunohost_api.conf.inc +++ b/data/templates/nginx/plain/yunohost_api.conf.inc @@ -4,6 +4,7 @@ location /yunohost/api/ { proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; + proxy_set_header Host $http_host; # Custom 502 error page error_page 502 /yunohost/api/error/502; diff --git a/data/templates/nginx/server.tpl.conf b/data/templates/nginx/server.tpl.conf index 78909e3f6..9acc641ae 100644 --- a/data/templates/nginx/server.tpl.conf +++ b/data/templates/nginx/server.tpl.conf @@ -5,7 +5,7 @@ server { access_by_lua_file /usr/share/ssowat/access.lua; - include conf.d/{{ domain }}.d/*.conf; + include /etc/nginx/conf.d/{{ domain }}.d/*.conf; location /yunohost/admin { return 301 https://$http_host$request_uri; @@ -68,12 +68,21 @@ server { add_header X-Permitted-Cross-Domain-Policies none; add_header X-Frame-Options "SAMEORIGIN"; + {% if domain_cert_ca == "Let's Encrypt" %} + # OCSP settings + ssl_stapling on; + ssl_stapling_verify on; + ssl_trusted_certificate /etc/yunohost/certs/{{ domain }}/crt.pem; + resolver 127.0.0.1 127.0.1.1 valid=300s; + resolver_timeout 5s; + {% endif %} + access_by_lua_file /usr/share/ssowat/access.lua; - include conf.d/{{ domain }}.d/*.conf; + include /etc/nginx/conf.d/{{ domain }}.d/*.conf; - include conf.d/yunohost_admin.conf.inc; - include conf.d/yunohost_api.conf.inc; + include /etc/nginx/conf.d/yunohost_admin.conf.inc; + include /etc/nginx/conf.d/yunohost_api.conf.inc; access_log /var/log/nginx/{{ domain }}-access.log; error_log /var/log/nginx/{{ domain }}-error.log; diff --git a/data/templates/slapd/ldap.conf b/data/templates/slapd/ldap.conf index 406b5c175..09aeb8b4f 100644 --- a/data/templates/slapd/ldap.conf +++ b/data/templates/slapd/ldap.conf @@ -5,8 +5,8 @@ # See ldap.conf(5) for details # This file should be world readable but not world writable. -#BASE dc=example,dc=com -#URI ldap://ldap.example.com ldap://ldap-master.example.com:666 +BASE dc=yunohost,dc=org +URI ldap://localhost:389 #SIZELIMIT 12 #TIMELIMIT 15 diff --git a/data/templates/ssl/openssl.cnf b/data/templates/ssl/openssl.cnf index fa5d19fa3..ac8c422e3 100644 --- a/data/templates/ssl/openssl.cnf +++ b/data/templates/ssl/openssl.cnf @@ -43,7 +43,7 @@ unique_subject = no # Set to 'no' to allow creation of new_certs_dir = $dir/newcerts # default place for new certs. certificate = $dir/ca/cacert.pem # The CA certificate -serial = $dir/serial # The current serial number +#serial = $dir/serial # The current serial number #crlnumber = $dir/crlnumber # the current crl number # must be commented out to leave a V1 CRL crl = $dir/crl.pem # The current CRL diff --git a/debian/changelog b/debian/changelog index a24f5c054..1562c4d2f 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,39 @@ +yunohost (3.2.2) stable; urgency=low + + * [hotfix] mod_auth_ldap: reflect SASL API changes in latest Metronome (#546) + * [enh] Add the internal helper ynh_handle_getopts_args (#520) + + Thanks to all contributors (Maranda, Maniack) ! <3 + + -- Alexandre Aubin Fri, 28 Sep 2018 23:04:00 +0000 + +yunohost (3.2.1) stable; urgency=low + + * Don't send an email if no certificate needs to be renewed (#540) + * Fix an issue with home backups (#541) + * Fix an issue with installs on OVH VPS + * Tell the user about post-install available in browser in bootprompt (#544) + * Improve Arabic translation + + Thanks to all contributors (BoF, ljf, Aleks) ! <3 + + -- Alexandre Aubin Mon, 17 Sep 2018 18:06:00 +0000 + +yunohost (3.2.0) stable; urgency=low + + * Add many print and exec helpers (#523) + * Add su directive as option for logrotate helper (#511) + * Add equivs, fake-hwclock and jq as base dependencies (#515, #514, #532) + * Allow to add a service description on "yunohost service add" (#529) + * Add option '--need-lock' to 'yunohost service add' (#530) + * Don't backup user home with .nobackup file (#536) + * Add a script to automatically generate helpers documentation (#538) + * [i18n] Improve Arabic translation + + Thanks to all contributors (Bram, Maniack, irina11y, Josue, BoF, ljf, Aleks) ! <3 + + -- Alexandre Aubin Tue, 11 Sep 2018 16:30:00 +0000 + yunohost (3.2.0~testing1) testing; urgency=low * Add logging system of every unit operation (#165) diff --git a/debian/control b/debian/control index cf450484e..be63436da 100644 --- a/debian/control +++ b/debian/control @@ -14,7 +14,7 @@ Depends: ${python:Depends}, ${misc:Depends} , python-psutil, python-requests, python-dnspython, python-openssl , python-apt, python-miniupnpc, python-dbus, python-jinja2 , glances - , dnsutils, bind9utils, unzip, git, curl, cron, wget + , dnsutils, bind9utils, unzip, git, curl, cron, wget, jq , ca-certificates, netcat-openbsd, iproute , mariadb-server, php-mysql | php-mysqlnd , slapd, ldap-utils, sudo-ldap, libnss-ldapd, unscd @@ -25,7 +25,7 @@ Depends: ${python:Depends}, ${misc:Depends} , dnsmasq, openssl, avahi-daemon, libnss-mdns, resolvconf, libnss-myhostname , metronome , rspamd (>= 1.6.0), redis-server, opendkim-tools - , haveged + , haveged, fake-hwclock , equivs Recommends: yunohost-admin , openssh-server, ntp, inetutils-ping | iputils-ping diff --git a/debian/install b/debian/install index e9c79e963..b540ca749 100644 --- a/debian/install +++ b/debian/install @@ -4,6 +4,7 @@ data/bash-completion.d/yunohost /etc/bash_completion.d/ data/actionsmap/* /usr/share/moulinette/actionsmap/ data/hooks/* /usr/share/yunohost/hooks/ data/other/yunoprompt.service /etc/systemd/system/ +data/other/password/* /usr/share/yunohost/other/password/ data/other/* /usr/share/yunohost/yunohost-config/moulinette/ data/templates/* /usr/share/yunohost/templates/ data/helpers /usr/share/yunohost/ diff --git a/doc/generate_helper_doc.py b/doc/generate_helper_doc.py new file mode 100644 index 000000000..20022e253 --- /dev/null +++ b/doc/generate_helper_doc.py @@ -0,0 +1,171 @@ +#!/usr/env/python2.7 + +import os +import glob +import datetime + +def render(data): + + from jinja2 import Template + from ansi2html import Ansi2HTMLConverter + from ansi2html.style import get_styles + + conv = Ansi2HTMLConverter() + shell_css = "\n".join(map(str, get_styles(conv.dark_bg, conv.scheme))) + + def shell_to_html(shell): + return conv.convert(shell, False) + + template = open("helper_doc_template.html", "r").read() + t = Template(template) + t.globals['now'] = datetime.datetime.utcnow + result = t.render(data=data, convert=shell_to_html, shell_css=shell_css) + open("helpers.html", "w").write(result) + +############################################################################## + +class Parser(): + + def __init__(self, filename): + + self.file = open(filename, "r").readlines() + self.blocks = None + + def parse_blocks(self): + + self.blocks = [] + + current_reading = "void" + current_block = { "name": None, + "line": -1, + "comments": [], + "code": [] } + + for i, line in enumerate(self.file): + + line = line.rstrip().replace("\t", " ") + + if current_reading == "void": + if is_global_comment(line): + # We start a new comment bloc + current_reading = "comments" + assert line.startswith("# ") or line == "#", malformed_error(i) + current_block["comments"].append(line[2:]) + else: + pass + #assert line == "", malformed_error(i) + continue + + elif current_reading == "comments": + if is_global_comment(line): + # We're still in a comment bloc + assert line.startswith("# ") or line == "#", malformed_error(i) + current_block["comments"].append(line[2:]) + else: + # We're getting out of a comment bloc, we should find + # the name of the function + assert len(line.split()) >= 1 + current_block["line"] = i + current_block["name"] = line.split()[0].strip("(){") + # Then we expect to read the function + current_reading = "code" + + continue + + elif current_reading == "code": + + if line == "}": + # We're getting out of the function + current_reading = "void" + + # Then we keep this bloc and start a new one + # (we ignore helpers containing [internal] ...) + if not "[internal]" in current_block["comments"]: + self.blocks.append(current_block) + current_block = { "name": None, + "line": -1, + "comments": [], + "code": [] } + else: + current_block["code"].append(line) + pass + + continue + + def parse_block(self, b): + + b["brief"] = "" + b["details"] = "" + b["usage"] = "" + b["args"] = [] + b["ret"] = "" + b["example"] = "" + + subblocks = '\n'.join(b["comments"]).split("\n\n") + + for i, subblock in enumerate(subblocks): + subblock = subblock.strip() + + if i == 0: + b["brief"] = subblock + continue + + elif subblock.startswith("example"): + b["example"] = " ".join(subblock.split()[1:]) + continue + + elif subblock.startswith("usage"): + for line in subblock.split("\n"): + + if line.startswith("| arg"): + argname = line.split()[2] + argdescr = " ".join(line.split()[4:]) + b["args"].append((argname, argdescr)) + elif line.startswith("| ret"): + b["ret"] = " ".join(line.split()[2:]) + else: + if line.startswith("usage"): + line = " ".join(line.split()[1:]) + b["usage"] += line + "\n" + continue + + elif subblock.startswith("| arg"): + for line in subblock.split("\n"): + if line.startswith("| arg"): + argname = line.split()[2] + argdescr = line.split()[4:] + b["args"].append((argname, argdescr)) + continue + + else: + b["details"] += subblock + "\n\n" + + b["usage"] = b["usage"].strip() + + +def is_global_comment(line): + return line.startswith('#') + +def malformed_error(line_number): + import pdb; pdb.set_trace() + return "Malformed file line {} ?".format(line_number) + +def main(): + + helper_files = sorted(glob.glob("../data/helpers.d/*")) + helpers = [] + + for helper_file in helper_files: + category_name = os.path.basename(helper_file) + print "Parsing %s ..." % category_name + p = Parser(helper_file) + p.parse_blocks() + for b in p.blocks: + p.parse_block(b) + + helpers.append((category_name, p.blocks)) + + render(helpers) + +main() + diff --git a/doc/helper_doc_template.html b/doc/helper_doc_template.html new file mode 100644 index 000000000..1fa1f68ad --- /dev/null +++ b/doc/helper_doc_template.html @@ -0,0 +1,93 @@ + + +

App helpers

+ +{% for category, helpers in data %} + +

{{ category }}

+ +{% for h in helpers %} + +
+
+
+
{{ h.name }}
+
{{ h.brief }}
+
+
+
+

+ {% if not '\n' in h.usage %} + Usage: {{ h.usage }} + {% else %} + Usage: {{ h.usage }} + {% endif %} +

+ {% if h.args %} +

+ Arguments: +

    + {% for name, descr in h.args %} +
  • {{ name }} : {{ descr }}
  • + {% endfor %} +
+

+ {% endif %} + {% if h.ret %} +

+ Returns: {{ h.ret }} +

+ {% endif %} + {% if h.example %} +

+ Example: {{ h.example }} +

+ {% endif %} + {% if h.details %} +

+ Details: +

+ {{ h.details.replace('\n', '
') }} +

+

+ {% endif %} +

+ Dude, show me the code ! +

+ +
+
+ +
+ +{% endfor %} +{% endfor %} + + + diff --git a/lib/metronome/modules/mod_auth_ldap2.lua b/lib/metronome/modules/mod_auth_ldap2.lua index bb62ca546..f961885da 100644 --- a/lib/metronome/modules/mod_auth_ldap2.lua +++ b/lib/metronome/modules/mod_auth_ldap2.lua @@ -56,8 +56,9 @@ function new_default_provider(host) return nil, "Account creation/modification not available with LDAP."; end - function provider.get_sasl_handler() + function provider.get_sasl_handler(session) local testpass_authentication_profile = { + session = session, plain_test = function(sasl, username, password, realm) return provider.test_password(username, password), true; end, diff --git a/locales/ar.json b/locales/ar.json index cda9c2c8b..f300ec864 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -296,8 +296,8 @@ "restore_system_part_failed": "Unable to restore the '{part:s}' system part", "server_shutdown": "سوف ينطفئ الخادوم", "server_shutdown_confirm": "سوف ينطفئ الخادوم حالا. متأكد ؟ [{answers:s}]", - "server_reboot": "The server will reboot", - "server_reboot_confirm": "The server will reboot immediatly, are you sure? [{answers:s}]", + "server_reboot": "سيعاد تشغيل الخادوم", + "server_reboot_confirm": "سيعاد تشغيل الخادوم في الحين. هل أنت متأكد ؟ [{answers:s}]", "service_add_failed": "تعذرت إضافة خدمة '{service:s}'", "service_added": "The service '{service:s}' has been added", "service_already_started": "Service '{service:s}' has already been started", diff --git a/locales/ca.json b/locales/ca.json new file mode 100644 index 000000000..6c06d55b3 --- /dev/null +++ b/locales/ca.json @@ -0,0 +1,25 @@ +{ + "action_invalid": "Acció '{action:s}' invàlida", + "admin_password": "Contrasenya d'administració", + "admin_password_change_failed": "No s'ha pogut canviar la contrasenya", + "admin_password_changed": "S'ha canviat la contrasenya d'administració", + "app_already_installed": "{app:s} ja està instal·lada", + "app_already_installed_cant_change_url": "Aquesta aplicació ja està instal·lada. La URL no és pot canviar únicament amb aquesta funció. Mireu a \"app changeurl\" si està disponible.", + "app_already_up_to_date": "{app:s} ja està actualitzada", + "app_argument_choice_invalid": "Aquesta opció no és vàlida per l'argument '{name:s}', ha de ser una de {choices:s}", + "app_argument_invalid": "Valor invàlid per l'argument '{name:s}':{error:s}", + "app_argument_required": "Es necessita l'argument '{name:s}'", + "app_change_no_change_url_script": "L'aplicació {app_name:s} encara no permet canviar la seva URL, es possible que s'hagi d'actualitzar.", + "app_change_url_failed_nginx_reload": "No s'ha pogut tornar a carregar nginx. Aquí teniu el resultat de \"nginx -t\":\n{nginx_errors:s}", + "app_change_url_identical_domains": "L'antic i el nou domini/camí són idèntics ('{domain:s}{path:s}'), no hi ha res per fer.", + "app_change_url_no_script": "Aquesta aplicació '{app_name:s}' encara no permet modificar la URL. Potser s'ha d'actualitzar l'aplicació.", + "app_change_url_success": "La URL de {app:s} s'ha canviat correctament a {domain:s}{path:s}", + "app_extraction_failed": "No s'han pogut extreure els fitxers d'instal·lació", + "app_id_invalid": "Id de l'aplicació incorrecte", + "app_incompatible": "L'aplicació {app} no és compatible amb la teva versió de YunoHost", + "app_install_files_invalid": "Fitxers d'instal·lació invàlids", + "app_location_already_used": "L'aplicació '{app}' ja està instal·lada en aquest camí ({path})", + "app_make_default_location_already_used": "No es pot fer l'aplicació '{app}' per defecte en el domini {domain} ja que ja és utilitzat per una altra aplicació '{other_app}'", + "app_location_install_failed": "No s'ha pogut instal·lar l'aplicació en aquest camí ja que entra en conflicte amb l'aplicació '{other_app}' ja instal·lada a '{other_path}'", + "app_location_unavailable": "Aquesta url no està disponible o entra en conflicte amb aplicacions ja instal·lades:\n{apps:s}" +} diff --git a/locales/en.json b/locales/en.json index 703db0b7e..6ce22ca80 100644 --- a/locales/en.json +++ b/locales/en.json @@ -130,7 +130,6 @@ "certmanager_hit_rate_limit": "Too many certificates already issued for exact set of domains {domain:s} recently. Please try again later. See https://letsencrypt.org/docs/rate-limits/ for more details", "certmanager_http_check_timeout": "Timed out when server tried to contact itself through HTTP using public IP address (domain {domain:s} with ip {ip:s}). You may be experiencing hairpinning issue or the firewall/router ahead of your server is misconfigured.", "certmanager_no_cert_file": "Unable to read certificate file for domain {domain:s} (file: {file:s})", - "certmanager_old_letsencrypt_app_detected": "\nYunohost detected that the 'letsencrypt' app is installed, which conflits with the new built-in certificate management features in Yunohost. If you wish to use the new built-in features, please run the following commands to migrate your installation:\n\n yunohost app remove letsencrypt\n yunohost domain cert-install\n\nN.B.: this will attempt to re-install certificates for all domains with a Let's Encrypt certificate or self-signed certificate", "certmanager_self_ca_conf_file_not_found": "Configuration file not found for self-signing authority (file: {file:s})", "certmanager_unable_to_parse_self_CA_name": "Unable to parse name of self-signing authority (file: {file:s})", "custom_app_url_required": "You must provide a URL to upgrade your custom app {app:s}", @@ -197,6 +196,8 @@ "global_settings_setting_example_string": "Example string option", "global_settings_unknown_setting_from_settings_file": "Unknown key in settings: '{setting_key:s}', discarding it and save it in /etc/yunohost/unkown_settings.json", "global_settings_unknown_type": "Unexpected situation, the setting {setting:s} appears to have the type {unknown_type:s} but it's not a type supported by the system.", + "good_practices_about_admin_password": "You are now about to define a new administration password. The password should be at least 8 characters - though it is good practice to use longer password (i.e. a passphrase) and/or to use various kind of characters (uppercase, lowercase, digits and special characters).", + "good_practices_about_user_password": "You are now about to define a new user password. The password should be at least 8 characters - though it is good practice to use longer password (i.e. a passphrase) and/or to use various kind of characters (uppercase, lowercase, digits and special characters).", "hook_exec_failed": "Script execution failed: {path:s}", "hook_exec_not_terminated": "Script execution hasn\u2019t terminated: {path:s}", "hook_list_by_invalid": "Invalid property to list hook by", @@ -256,6 +257,7 @@ "mail_domain_unknown": "Unknown mail address domain '{domain:s}'", "mail_forward_remove_failed": "Unable to remove mail forward '{mail:s}'", "mailbox_used_space_dovecot_down": "Dovecot mailbox service need to be up, if you want to get mailbox used space", + "mail_unavailable": "This email address is reserved and shall be automatically allocated to the very first user", "maindomain_change_failed": "Unable to change the main domain", "maindomain_changed": "The main domain has been changed", "migrate_tsig_end": "Migration to hmac-sha512 finished", @@ -332,6 +334,11 @@ "packages_no_upgrade": "There is no package to upgrade", "packages_upgrade_critical_later": "Critical packages ({packages:s}) will be upgraded later", "packages_upgrade_failed": "Unable to upgrade all of the packages", + "password_listed": "This password is among the most used password in the world. Please choose something a bit more unique.", + "password_too_simple_1": "Password needs to be at least 8 characters long", + "password_too_simple_2": "Password needs to be at least 8 characters long and contains digit, upper and lower characters", + "password_too_simple_3": "Password needs to be at least 8 characters long and contains digit, upper, lower and special characters", + "password_too_simple_4": "Password needs to be at least 12 characters long and contains digit, upper, lower and special characters", "path_removal_failed": "Unable to remove path {:s}", "pattern_backup_archive_name": "Must be a valid filename with max 30 characters, and alphanumeric and -_. characters only", "pattern_domain": "Must be a valid domain name (e.g. my-domain.org)", diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 1fed09425..efdac70d7 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -346,7 +346,8 @@ def app_info(app, show_status=False, raw=False): ret['settings'] = _get_app_settings(app) # Determine upgradability - local_update_time = ret['settings'].get('update_time', ret['settings']['install_time']) + # In case there is neither update_time nor install_time, we assume the app can/has to be upgraded + local_update_time = ret['settings'].get('update_time', ret['settings'].get('install_time', 0)) if 'lastUpdate' not in ret or 'git' not in ret: upgradable = "url_required" @@ -513,7 +514,7 @@ def app_change_url(operation_logger, auth, app, domain, path): os.system('chmod +x %s' % os.path.join(os.path.join(APP_TMP_FOLDER, "scripts", "change_url"))) if hook_exec(os.path.join(APP_TMP_FOLDER, 'scripts/change_url'), - args=args_list, env=env_dict, user="root") != 0: + args=args_list, env=env_dict) != 0: msg = "Failed to change '%s' url." % app logger.error(msg) operation_logger.error(msg) @@ -640,7 +641,7 @@ def app_upgrade(auth, app=[], url=None, file=None): # Execute App upgrade script os.system('chown -hR admin: %s' % INSTALL_TMP) if hook_exec(extracted_app_folder + '/scripts/upgrade', - args=args_list, env=env_dict, user="root") != 0: + args=args_list, env=env_dict) != 0: msg = m18n.n('app_upgrade_failed', app=app_instance_name) logger.error(msg) operation_logger.error(msg) @@ -800,7 +801,7 @@ def app_install(operation_logger, auth, app, label=None, args=None, no_remove_on try: install_retcode = hook_exec( os.path.join(extracted_app_folder, 'scripts/install'), - args=args_list, env=env_dict, user="root" + args=args_list, env=env_dict ) except (KeyboardInterrupt, EOFError): install_retcode = -1 @@ -824,7 +825,7 @@ def app_install(operation_logger, auth, app, label=None, args=None, no_remove_on remove_retcode = hook_exec( os.path.join(extracted_app_folder, 'scripts/remove'), - args=[app_instance_name], env=env_dict_remove, user="root" + args=[app_instance_name], env=env_dict_remove ) if remove_retcode != 0: msg = m18n.n('app_not_properly_removed', @@ -912,7 +913,7 @@ def app_remove(operation_logger, auth, app): operation_logger.flush() if hook_exec('/tmp/yunohost_remove/scripts/remove', args=args_list, - env=env_dict, user="root") == 0: + env=env_dict) == 0: logger.success(m18n.n('app_removed', app=app)) hook_callback('post_app_remove', args=args_list, env=env_dict) @@ -1461,33 +1462,33 @@ def app_change_label(auth, app, new_label): # actions todo list: # * docstring -def app_action_list(app_id): +def app_action_list(app): logger.warning(m18n.n('experimental_feature')) # this will take care of checking if the app is installed - app_info_dict = app_info(app_id) + app_info_dict = app_info(app) - actions = os.path.join(APPS_SETTING_PATH, app_id, 'actions.json') + actions = os.path.join(APPS_SETTING_PATH, app, 'actions.json') return { - "app_id": app_id, + "app": app, "app_name": app_info_dict["name"], "actions": read_json(actions) if os.path.exists(actions) else [], } -def app_action_run(app_id, action, args=None): +def app_action_run(app, action, args=None): logger.warning(m18n.n('experimental_feature')) from yunohost.hook import hook_exec import tempfile # will raise if action doesn't exist - actions = app_action_list(app_id)["actions"] + actions = app_action_list(app)["actions"] actions = {x["id"]: x for x in actions} if action not in actions: - raise MoulinetteError(errno.EINVAL, "action '%s' not available for app '%s', available actions are: %s" % (action, app_id, ", ".join(actions.keys()))) + raise MoulinetteError(errno.EINVAL, "action '%s' not available for app '%s', available actions are: %s" % (action, app, ", ".join(actions.keys()))) action_declaration = actions[action] @@ -1496,8 +1497,12 @@ def app_action_run(app_id, action, args=None): args_odict = _parse_args_for_action(actions[action], args=args_dict) args_list = args_odict.values() + app_id, app_instance_nb = _parse_app_instance_name(app) + env_dict = _make_environment_dict(args_odict, prefix="ACTION_") env_dict["YNH_APP_ID"] = app_id + env_dict["YNH_APP_INSTANCE_NAME"] = app + env_dict["YNH_APP_INSTANCE_NUMBER"] = str(app_instance_nb) env_dict["YNH_ACTION"] = action _, path = tempfile.mkstemp() @@ -1508,9 +1513,9 @@ def app_action_run(app_id, action, args=None): os.chmod(path, 700) if action_declaration.get("cwd"): - cwd = action_declaration["cwd"].replace("$app_id", app_id) + cwd = action_declaration["cwd"].replace("$app", app_id) else: - cwd = "/etc/yunohost/apps/" + app_id + cwd = "/etc/yunohost/apps/" + app retcode = hook_exec( path, @@ -1521,7 +1526,7 @@ def app_action_run(app_id, action, args=None): ) if retcode not in action_declaration.get("accepted_return_codes", [0]): - raise MoulinetteError(retcode, "Error while executing action '%s' of app '%s': return code %s" % (action, app_id, retcode)) + raise MoulinetteError(retcode, "Error while executing action '%s' of app '%s': return code %s" % (action, app, retcode)) os.remove(path) @@ -1531,25 +1536,34 @@ def app_action_run(app_id, action, args=None): # Config panel todo list: # * docstrings # * merge translations on the json once the workflow is in place -def app_config_show_panel(app_id): +def app_config_show_panel(app): logger.warning(m18n.n('experimental_feature')) from yunohost.hook import hook_exec # this will take care of checking if the app is installed - app_info_dict = app_info(app_id) + app_info_dict = app_info(app) - config_panel = os.path.join(APPS_SETTING_PATH, app_id, 'config_panel.json') - config_script = os.path.join(APPS_SETTING_PATH, app_id, 'scripts', 'config') + config_panel = os.path.join(APPS_SETTING_PATH, app, 'config_panel.json') + config_script = os.path.join(APPS_SETTING_PATH, app, 'scripts', 'config') + + app_id, app_instance_nb = _parse_app_instance_name(app) if not os.path.exists(config_panel) or not os.path.exists(config_script): return { + "app_id": app_id, + "app": app, + "app_name": app_info_dict["name"], "config_panel": [], } config_panel = read_json(config_panel) - env = {"YNH_APP_ID": app_id} + env = { + "YNH_APP_ID": app_id, + "YNH_APP_INSTANCE_NAME": app, + "YNH_APP_INSTANCE_NUMBER": str(app_instance_nb), + } parsed_values = {} # I need to parse stdout to communicate between scripts because I can't @@ -1573,7 +1587,6 @@ def app_config_show_panel(app_id): return_code = hook_exec(config_script, args=["show"], env=env, - user="root", stdout_callback=parse_stdout, ) @@ -1607,23 +1620,24 @@ def app_config_show_panel(app_id): return { "app_id": app_id, + "app": app, "app_name": app_info_dict["name"], "config_panel": config_panel, } -def app_config_apply(app_id, args): +def app_config_apply(app, args): logger.warning(m18n.n('experimental_feature')) from yunohost.hook import hook_exec - installed = _is_installed(app_id) + installed = _is_installed(app) if not installed: raise MoulinetteError(errno.ENOPKG, - m18n.n('app_not_installed', app=app_id)) + m18n.n('app_not_installed', app=app)) - config_panel = os.path.join(APPS_SETTING_PATH, app_id, 'config_panel.json') - config_script = os.path.join(APPS_SETTING_PATH, app_id, 'scripts', 'config') + config_panel = os.path.join(APPS_SETTING_PATH, app, 'config_panel.json') + config_script = os.path.join(APPS_SETTING_PATH, app, 'scripts', 'config') if not os.path.exists(config_panel) or not os.path.exists(config_script): # XXX real exception @@ -1631,7 +1645,12 @@ def app_config_apply(app_id, args): config_panel = read_json(config_panel) - env = {"YNH_APP_ID": app_id} + app_id, app_instance_nb = _parse_app_instance_name(app) + env = { + "YNH_APP_ID": app_id, + "YNH_APP_INSTANCE_NAME": app, + "YNH_APP_INSTANCE_NUMBER": str(app_instance_nb), + } args = dict(urlparse.parse_qsl(args, keep_blank_values=True)) if args else {} for tab in config_panel.get("panel", []): @@ -1656,7 +1675,6 @@ def app_config_apply(app_id, args): return_code = hook_exec(config_script, args=["apply"], env=env, - user="root", ) if return_code != 0: @@ -2189,11 +2207,15 @@ def _parse_action_args_in_yunohost_format(args, action_args, auth=None): for domain in domain_list(auth)['domains']: msignals.display("- {}".format(domain)) - if arg_type == 'user': + elif arg_type == 'user': msignals.display(m18n.n('users_available')) for user in user_list(auth)['users'].keys(): msignals.display("- {}".format(user)) + elif arg_type == 'password': + msignals.display(m18n.n('good_practices_about_user_password')) + + try: input_string = msignals.prompt(ask_string, is_password) except NotImplementedError: @@ -2251,6 +2273,9 @@ def _parse_action_args_in_yunohost_format(args, action_args, auth=None): raise MoulinetteError(errno.EINVAL, m18n.n('app_argument_choice_invalid', name=arg_name, choices='yes, no, y, n, 1, 0')) + elif arg_type == 'password': + from yunohost.utils.password import assert_password_is_strong_enough + assert_password_is_strong_enough('user', arg_value) args_dict[arg_name] = arg_value # END loop over action_args... diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index 88959cc2f..6ef9a0554 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -680,7 +680,7 @@ class BackupManager(): subprocess.call(['install', '-Dm555', app_script, tmp_script]) hook_exec(tmp_script, args=[tmp_app_bkp_dir, app], - raise_on_error=True, chdir=tmp_app_bkp_dir, env=env_dict, user="root") + raise_on_error=True, chdir=tmp_app_bkp_dir, env=env_dict) self._import_to_list_to_backup(env_dict["YNH_BACKUP_CSV"]) except: @@ -1310,8 +1310,7 @@ class RestoreManager(): args=[app_backup_in_archive, app_instance_name], chdir=app_backup_in_archive, raise_on_error=True, - env=env_dict, - user="root") + env=env_dict) except: msg = m18n.n('restore_app_failed',app=app_instance_name) logger.exception(msg) @@ -1336,7 +1335,7 @@ class RestoreManager(): # Execute remove script # TODO: call app_remove instead if hook_exec(remove_script, args=[app_instance_name], - env=env_dict_remove, user="root") != 0: + env=env_dict_remove) != 0: msg = m18n.n('app_not_properly_removed', app=app_instance_name) logger.warning(msg) operation_logger.error(msg) @@ -2188,7 +2187,7 @@ def backup_list(with_info=False, human_readable=False): except ValueError: continue result.append(name) - result.sort() + result.sort(key=lambda x: os.path.getctime(os.path.join(ARCHIVES_PATH, x+".tar.gz"))) if result and with_info: d = OrderedDict() diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 1b80b6b49..049eeb0f4 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -97,11 +97,6 @@ def certificate_status(auth, domain_list, full=False): import yunohost.domain - # Check if old letsencrypt_ynh is installed - # TODO / FIXME - Remove this in the future once the letsencrypt app is - # not used anymore - _check_old_letsencrypt_app() - # If no domains given, consider all yunohost domains if domain_list == []: domain_list = yunohost.domain.domain_list(auth)['domains'] @@ -144,11 +139,6 @@ def certificate_install(auth, domain_list, force=False, no_checks=False, self_si self-signed -- Instal self-signed certificates instead of Let's Encrypt """ - # Check if old letsencrypt_ynh is installed - # TODO / FIXME - Remove this in the future once the letsencrypt app is - # not used anymore - _check_old_letsencrypt_app() - if self_signed: _certificate_install_selfsigned(domain_list, force) else: @@ -328,11 +318,6 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal import yunohost.domain - # Check if old letsencrypt_ynh is installed - # TODO / FIXME - Remove this in the future once the letsencrypt app is - # not used anymore - _check_old_letsencrypt_app() - # If no domains given, consider all yunohost domains with Let's Encrypt # certificates if domain_list == []: @@ -355,7 +340,7 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal domain_list.append(domain) - if len(domain_list) == 0: + if len(domain_list) == 0 and not email: logger.info("No certificate needs to be renewed.") # Else, validate the domain list given @@ -430,18 +415,6 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal # Back-end stuff # ############################################################################### -def _check_old_letsencrypt_app(): - import yunohost.domain - - installedAppIds = [app["id"] for app in yunohost.app.app_list(installed=True)["apps"]] - - if "letsencrypt" not in installedAppIds: - return - - raise MoulinetteError(errno.EINVAL, m18n.n( - 'certmanager_old_letsencrypt_app_detected')) - - def _install_cron(): cron_job_file = "/etc/cron.daily/yunohost-certificate-renew" diff --git a/src/yunohost/data_migrations/0003_migrate_to_stretch.py b/src/yunohost/data_migrations/0003_migrate_to_stretch.py index 7347f0e66..26f91ae0b 100644 --- a/src/yunohost/data_migrations/0003_migrate_to_stretch.py +++ b/src/yunohost/data_migrations/0003_migrate_to_stretch.py @@ -94,7 +94,7 @@ class MyMigration(Migration): # with /etc/lsb-release for instance -_-) # Instead, we rely on /etc/os-release which should be the raw info from # the distribution... - return int(check_output("grep VERSION_ID /etc/os-release | tr '\"' ' ' | cut -d ' ' -f2")) + return int(check_output("grep VERSION_ID /etc/os-release | head -n 1 | tr '\"' ' ' | cut -d ' ' -f2")) def yunohost_major_version(self): return int(get_installed_version("yunohost").split('.')[0]) diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 560a6fda5..685e60517 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -209,6 +209,11 @@ def domain_dns_conf(domain, ttl=None): for record in dns_conf["mail"]: result += "\n{name} {ttl} IN {type} {value}".format(**record) + result += "\n\n" + result += "; Extra" + for record in dns_conf["extra"]: + result += "\n{name} {ttl} IN {type} {value}".format(**record) + is_cli = True if msettings.get('interface') == 'cli' else False if is_cli: logger.info(m18n.n("domain_dns_conf_is_just_a_recommendation")) @@ -334,6 +339,9 @@ def _build_dns_conf(domain, ttl=3600): {"type": "TXT", "name": "mail._domainkey", "value": "\"v=DKIM1; k=rsa; p=some-super-long-key\"", "ttl": 3600}, {"type": "TXT", "name": "_dmarc", "value": "\"v=DMARC1; p=none\"", "ttl": 3600} ], + "extra": [ + {"type": "CAA", "name": "@", "value": "128 issue 'letsencrypt.org", "ttl": 3600}, + ], } """ @@ -387,10 +395,16 @@ def _build_dns_conf(domain, ttl=3600): ["_dmarc", ttl, "TXT", '"v=DMARC1; p=none"'], ] + # Extra + extra = [ + ["@", ttl, "CAA", "128 issue 'letsencrypt.org'"] + ] + return { "basic": [{"name": name, "ttl": ttl, "type": type_, "value": value} for name, ttl, type_, value in basic], "xmpp": [{"name": name, "ttl": ttl, "type": type_, "value": value} for name, ttl, type_, value in xmpp], "mail": [{"name": name, "ttl": ttl, "type": type_, "value": value} for name, ttl, type_, value in mail], + "extra": [{"name": name, "ttl": ttl, "type": type_, "value": value} for name, ttl, type_, value in extra], } diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index 88547b4db..3e040d682 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -37,6 +37,8 @@ from moulinette.core import MoulinetteError 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 moulinette.utils.process import check_output + from yunohost.domain import _get_maindomain, _build_dns_conf from yunohost.utils.network import get_public_ip @@ -44,8 +46,6 @@ from yunohost.log import is_unit_operation logger = getActionLogger('yunohost.dyndns') -OLD_IPV4_FILE = '/etc/yunohost/dyndns/old_ip' -OLD_IPV6_FILE = '/etc/yunohost/dyndns/old_ipv6' DYNDNS_ZONE = '/etc/yunohost/dyndns/zone' RE_DYNDNS_PRIVATE_KEY_MD5 = re.compile( @@ -96,7 +96,7 @@ def _dyndns_available(provider, domain): domain -- The full domain that you'd like.. e.g. "foo.nohost.me" Returns: - True if the domain is avaible, False otherwise. + True if the domain is available, False otherwise. """ logger.debug("Checking if domain %s is available on %s ..." % (domain, provider)) @@ -193,32 +193,6 @@ def dyndns_update(operation_logger, dyn_host="dyndns.yunohost.org", domain=None, 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_ = get_public_ip() - ipv6_ = get_public_ip(6) - - if ipv4 is None: - ipv4 = ipv4_ - - if ipv6 is None: - ipv6 = ipv6_ - - logger.debug("Old IPv4/v6 are (%s, %s)" % (old_ipv4, old_ipv6)) - logger.debug("Requested IPv4/v6 are (%s, %s)" % (ipv4, ipv6)) - - # no need to update - if old_ipv4 == ipv4 and old_ipv6 == ipv6: - logger.info("No updated needed.") - return - else: - logger.info("Updated needed, going on...") - # If domain is not given, try to guess it from keys available... if domain is None: (domain, key) = _guess_current_dyndns_domain(dyn_host) @@ -262,7 +236,32 @@ def dyndns_update(operation_logger, dyn_host="dyndns.yunohost.org", domain=None, 'zone %s' % host, ] + + old_ipv4 = check_output("dig @%s +short %s" % (dyn_host, domain)).strip() or None + old_ipv6 = check_output("dig @%s +short aaaa %s" % (dyn_host, domain)).strip() or None + + # Get current IPv4 and IPv6 + ipv4_ = get_public_ip() + ipv6_ = get_public_ip(6) + + if ipv4 is None: + ipv4 = ipv4_ + + if ipv6 is None: + ipv6 = ipv6_ + + logger.debug("Old IPv4/v6 are (%s, %s)" % (old_ipv4, old_ipv6)) + logger.debug("Requested IPv4/v6 are (%s, %s)" % (ipv4, ipv6)) + + # no need to update + if old_ipv4 == ipv4 and old_ipv6 == ipv6: + logger.info("No updated needed.") + return + else: + logger.info("Updated needed, going on...") + dns_conf = _build_dns_conf(domain) + del dns_conf["extra"] # Ignore records from the 'extra' category # Delete the old records for all domain/subdomains @@ -304,18 +303,11 @@ def dyndns_update(operation_logger, dyn_host="dyndns.yunohost.org", domain=None, 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, m18n.n('dyndns_ip_update_failed')) logger.success(m18n.n('dyndns_ip_updated')) - if ipv4 is not None: - write_to_file(OLD_IPV4_FILE, ipv4) - if ipv6 is not None: - write_to_file(OLD_IPV6_FILE, ipv6) - def dyndns_installcron(): """ diff --git a/src/yunohost/hook.py b/src/yunohost/hook.py index 87844ce17..46c25d50d 100644 --- a/src/yunohost/hook.py +++ b/src/yunohost/hook.py @@ -281,7 +281,7 @@ def hook_callback(action, hooks=[], args=None, no_trace=False, chdir=None, hook_args = pre_callback(name=name, priority=priority, path=path, args=args) hook_exec(path, args=hook_args, chdir=chdir, env=env, - no_trace=no_trace, raise_on_error=True, user="root") + no_trace=no_trace, raise_on_error=True) except MoulinetteError as e: state = 'failed' logger.error(e.strerror, exc_info=1) @@ -298,7 +298,7 @@ def hook_callback(action, hooks=[], args=None, no_trace=False, chdir=None, def hook_exec(path, args=None, raise_on_error=False, no_trace=False, - chdir=None, env=None, user="admin", stdout_callback=None, + chdir=None, env=None, user="root", stdout_callback=None, stderr_callback=None): """ Execute hook from a file with arguments diff --git a/src/yunohost/service.py b/src/yunohost/service.py index 66ae837a9..5b7680a80 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -50,7 +50,7 @@ MOULINETTE_LOCK = "/var/run/moulinette_yunohost.lock" logger = log.getActionLogger('yunohost.service') -def service_add(name, status=None, log=None, runlevel=None): +def service_add(name, status=None, log=None, runlevel=None, need_lock=False, description=None): """ Add a custom service @@ -59,7 +59,8 @@ def service_add(name, status=None, log=None, runlevel=None): status -- Custom status command log -- Absolute path to log file to display runlevel -- Runlevel priority of the service - + need_lock -- Use this option to prevent deadlocks if the service does invoke yunohost commands. + description -- description of the service """ services = _get_services() @@ -74,6 +75,12 @@ def service_add(name, status=None, log=None, runlevel=None): if runlevel is not None: services[name]['runlevel'] = runlevel + if need_lock: + services[name]['need_lock'] = True + + if description is not None: + services[name]['description'] = description + try: _save_services(services) except: @@ -251,7 +258,10 @@ def service_status(names=[]): else: translation_key = "service_description_%s" % name - description = m18n.n(translation_key) + if "description" in services[name] is not None: + description = services[name].get("description") + else: + description = m18n.n(translation_key) # that mean that we don't have a translation for this string # that's the only way to test for that for now diff --git a/src/yunohost/settings.py b/src/yunohost/settings.py index aba6e32b3..d2526316e 100644 --- a/src/yunohost/settings.py +++ b/src/yunohost/settings.py @@ -29,12 +29,16 @@ SETTINGS_PATH_OTHER_LOCATION = "/etc/yunohost/settings-%s.json" # * string # * enum (in form a python list) -# we don't store the value in default options DEFAULTS = OrderedDict([ ("example.bool", {"type": "bool", "default": True}), ("example.int", {"type": "int", "default": 42}), ("example.string", {"type": "string", "default": "yolo swag"}), ("example.enum", {"type": "enum", "default": "a", "choices": ["a", "b", "c"]}), + + # Password Validation + # -1 disabled, 0 alert if listed, 1 8-letter, 2 normal, 3 strong, 4 strongest + ("security.password.admin.strength", {"type": "int", "default": 1}), + ("security.password.user.strength", {"type": "int", "default": 1}), ]) @@ -90,9 +94,12 @@ def settings_set(key, value): received_type=type(value).__name__, expected_type=key_type)) elif key_type == "int": if not isinstance(value, int) or isinstance(value, bool): - raise MoulinetteError(errno.EINVAL, m18n.n( - 'global_settings_bad_type_for_setting', setting=key, - received_type=type(value).__name__, expected_type=key_type)) + if isinstance(value, str): + value=int(value) + else: + raise MoulinetteError(errno.EINVAL, m18n.n( + 'global_settings_bad_type_for_setting', setting=key, + received_type=type(value).__name__, expected_type=key_type)) elif key_type == "string": if not isinstance(value, basestring): raise MoulinetteError(errno.EINVAL, m18n.n( diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 9bd4c9e88..a34b41545 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -32,6 +32,7 @@ import logging import subprocess import pwd import socket +import cracklib from xmlrpclib import Fault from importlib import import_module from collections import OrderedDict @@ -127,8 +128,13 @@ def tools_adminpw(auth, new_password): """ from yunohost.user import _hash_user_password + from yunohost.utils.password import assert_password_is_strong_enough import spwd + + assert_password_is_strong_enough("admin", new_password) + new_hash = _hash_user_password(new_password) + try: auth.update("cn=admin", { "userPassword": new_hash, }) except: @@ -263,7 +269,8 @@ def _is_inside_container(): @is_unit_operation() -def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False): +def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False, + force_password=False): """ YunoHost post-install @@ -274,6 +281,8 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False): password -- YunoHost admin password """ + from yunohost.utils.password import assert_password_is_strong_enough + dyndns_provider = "dyndns.yunohost.org" # Do some checks at first @@ -281,6 +290,10 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False): raise MoulinetteError(errno.EPERM, m18n.n('yunohost_already_installed')) + # Check password + if not force_password: + assert_password_is_strong_enough("admin", password) + if not ignore_dyndns: # Check if yunohost dyndns can handle the given domain # (i.e. is it a .nohost.me ? a .noho.st ?) @@ -312,6 +325,7 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False): else: dyndns = False + operation_logger.start() logger.info(m18n.n('yunohost_installing')) @@ -371,7 +385,6 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False): service_regen_conf(['ssl'], force=True) ssl_dir = '/usr/share/yunohost/yunohost-config/ssl/yunoCA' commands = [ - 'echo "01" > %s/serial' % ssl_dir, 'rm %s/index.txt' % ssl_dir, 'touch %s/index.txt' % ssl_dir, 'cp %s/openssl.cnf %s/openssl.ca.cnf' % (ssl_dir, ssl_dir), @@ -1059,3 +1072,4 @@ class Migration(object): @property def description(self): return m18n.n("migration_description_%s" % self.id) + diff --git a/src/yunohost/user.py b/src/yunohost/user.py index 48065f70a..990ec4c8e 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -32,6 +32,7 @@ import crypt import random import string import subprocess +import cracklib from moulinette import m18n from moulinette.core import MoulinetteError @@ -116,6 +117,10 @@ def user_create(operation_logger, auth, username, firstname, lastname, mail, pas from yunohost.domain import domain_list, _get_maindomain from yunohost.hook import hook_callback from yunohost.app import app_ssowatconf + from yunohost.utils.password import assert_password_is_strong_enough + + # Ensure sufficiently complex password + assert_password_is_strong_enough("user", password) # Validate uniqueness of username and mail in LDAP auth.validate_uniqueness({ @@ -127,6 +132,17 @@ def user_create(operation_logger, auth, username, firstname, lastname, mail, pas all_existing_usernames = {x.pw_name for x in pwd.getpwall()} if username in all_existing_usernames: raise MoulinetteError(errno.EEXIST, m18n.n('system_username_exists')) + + main_domain = _get_maindomain() + aliases = [ + 'root@' + main_domain, + 'admin@' + main_domain, + 'webmaster@' + main_domain, + 'postmaster@' + main_domain, + ] + + if mail in aliases: + raise MoulinetteError(errno.EEXIST,m18n.n('mail_unavailable')) # Check that the mail domain exists if mail.split("@")[1] not in domain_list(auth)['domains']: @@ -166,13 +182,6 @@ def user_create(operation_logger, auth, username, firstname, lastname, mail, pas # If it is the first user, add some aliases if not auth.search(base='ou=users,dc=yunohost,dc=org', filter='uid=*'): - main_domain = _get_maindomain() - aliases = [ - 'root@' + main_domain, - 'admin@' + main_domain, - 'webmaster@' + main_domain, - 'postmaster@' + main_domain, - ] attr_dict['mail'] = [attr_dict['mail']] + aliases # If exists, remove the redirection from the SSO @@ -279,6 +288,7 @@ def user_update(operation_logger, auth, username, firstname=None, lastname=None, """ from yunohost.domain import domain_list from yunohost.app import app_ssowatconf + from yunohost.utils.password import assert_password_is_strong_enough attrs_to_fetch = ['givenName', 'sn', 'mail', 'maildrop'] new_attr_dict = {} @@ -303,14 +313,27 @@ def user_update(operation_logger, auth, username, firstname=None, lastname=None, new_attr_dict['cn'] = new_attr_dict['displayName'] = firstname + ' ' + lastname if change_password: + # Ensure sufficiently complex password + assert_password_is_strong_enough("user", password) + new_attr_dict['userPassword'] = _hash_user_password(change_password) if mail: + main_domain = _get_maindomain() + aliases = [ + 'root@' + main_domain, + 'admin@' + main_domain, + 'webmaster@' + main_domain, + 'postmaster@' + main_domain, + ] auth.validate_uniqueness({'mail': mail}) if mail[mail.find('@') + 1:] not in domains: raise MoulinetteError(errno.EINVAL, m18n.n('mail_domain_unknown', domain=mail[mail.find('@') + 1:])) + if mail in aliases: + raise MoulinetteError(errno.EEXIST,m18n.n('mail_unavailable')) + del user['mail'][0] new_attr_dict['mail'] = [mail] + user['mail'] diff --git a/src/yunohost/utils/password.py b/src/yunohost/utils/password.py new file mode 100644 index 000000000..68e51056b --- /dev/null +++ b/src/yunohost/utils/password.py @@ -0,0 +1,196 @@ +# -*- coding: utf-8 -*- + +""" License + + Copyright (C) 2018 YunoHost + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program; if not, see http://www.gnu.org/licenses + +""" + +import sys +import os +import json +import string +import subprocess + +SMALL_PWD_LIST = ["yunohost", "olinuxino", "olinux", "raspberry", "admin", + "root", "test", "rpi"] + +MOST_USED_PASSWORDS = '/usr/share/yunohost/other/password/100000-most-used.txt' + +# Length, digits, lowers, uppers, others +STRENGTH_LEVELS = [ + (8, 0, 0, 0, 0), + (8, 1, 1, 1, 0), + (8, 1, 1, 1, 1), + (12, 1, 1, 1, 1), +] + +def assert_password_is_strong_enough(profile, password): + PasswordValidator(profile).validate(password) + +class PasswordValidator(object): + + def __init__(self, profile): + """ + Initialize a password validator. + + The profile shall be either "user" or "admin" + and will correspond to a validation strength + defined via the setting "security.password..strength" + """ + + self.profile = profile + try: + # We do this "manually" instead of using settings_get() + # from settings.py because this file is also meant to be + # use as a script by ssowat. + # (or at least that's my understanding -- Alex) + settings = json.load(open('/etc/yunohost/settings.json', "r")) + setting_key = "security.password." + profile + ".strength" + self.validation_strength = int(settings[setting_key]) + except Exception as e: + # Fallback to default value if we can't fetch settings for some reason + self.validation_strength = 1 + + def validate(self, password): + """ + Check the validation_summary and trigger an exception + if the password does not pass tests. + + This method is meant to be used from inside YunoHost's code + (compared to validation_summary which is meant to be called + by ssowat) + """ + if self.validation_strength == -1: + return + + # Note that those imports are made here and can't be put + # on top (at least not the moulinette ones) + # because the moulinette needs to be correctly initialized + # as well as modules available in python's path. + import errno + import logging + from moulinette import m18n + from moulinette.core import MoulinetteError + from moulinette.utils.log import getActionLogger + + logger = logging.getLogger('yunohost.utils.password') + + status, msg = self.validation_summary(password) + if status == "error": + raise MoulinetteError(1, m18n.n(msg)) + + def validation_summary(self, password): + """ + Check if a password is listed in the list of most used password + and if the overall strength is good enough compared to the + validation_strength defined in the constructor. + + Produces a summary-tuple comprised of a level (succes or error) + and a message key describing the issues found. + """ + if self.validation_strength < 0: + return ("success", "") + + listed = password in SMALL_PWD_LIST or self.is_in_most_used_list(password) + strength_level = self.strength_level(password) + if listed: + return ("error", "password_listed") + if strength_level < self.validation_strength: + return ("error", "password_too_simple_%s" % self.validation_strength) + + return ("success", "") + + def strength(self, password): + """ + Returns the strength of a password, defined as a tuple + containing the length of the password, the number of digits, + lowercase letters, uppercase letters, and other characters. + + For instance, "PikachuDu67" is (11, 2, 7, 2, 0) + """ + + length = len(password) + digits = 0 + uppers = 0 + lowers = 0 + others = 0 + + for character in password: + if character in string.digits: + digits = digits + 1 + elif character in string.ascii_uppercase: + uppers = uppers + 1 + elif character in string.ascii_lowercase: + lowers = lowers + 1 + else: + others = others + 1 + + return (length, digits, lowers, uppers, others) + + def strength_level(self, password): + """ + Computes the strength of a password and compares + it to the STRENGTH_LEVELS. + + Returns an int corresponding to the highest STRENGTH_LEVEL + satisfied by the password. + """ + + strength = self.strength(password) + + strength_level = 0 + # Iterate over each level and its criterias + for level, level_criterias in enumerate(STRENGTH_LEVELS): + # Iterate simulatenously over the level criterias (e.g. [8, 1, 1, 1, 0]) + # and the strength of the password (e.g. [11, 2, 7, 2, 0]) + # and compare the values 1-by-1. + # If one False is found, the password does not satisfy the level + if False in [s>=c for s, c in zip(strength, level_criterias)]: + break + # Otherwise, the strength of the password is at least of the current level. + strength_level = level + 1 + + return strength_level + + def is_in_most_used_list(self, password): + + # Decompress file if compressed + if os.path.exists("%s.gz" % MOST_USED_PASSWORDS): + os.system("gzip -fd %s.gz" % MOST_USED_PASSWORDS) + + # Grep the password in the file + # We use '-f -' to feed the pattern (= the password) through + # stdin to avoid it being shown in ps -ef --forest... + command = "grep -q -f - %s" % MOST_USED_PASSWORDS + p = subprocess.Popen(command.split(), stdin=subprocess.PIPE) + p.communicate(input=password) + return not bool(p.returncode) + + +# This file is also meant to be used as an executable by +# SSOwat to validate password from the portal when an user +# change its password. +if __name__ == '__main__': + if len(sys.argv) < 2: + import getpass + pwd = getpass.getpass("") + #print("usage: password.py PASSWORD") + else: + pwd = sys.argv[1] + status, msg = PasswordValidator('user').validation_summary(pwd) + print(msg) + sys.exit(0)