Compare commits

..

No commits in common. "debian/12.0.3" and "dev" have entirely different histories.

134 changed files with 6011 additions and 6220 deletions

View file

@ -1,7 +1,2 @@
[coverage:run]
relative_files = True
source = src/
branch = True
[report]
omit=src/tests/*,src/vendor/*,/usr/lib/moulinette/yunohost/*,/usr/lib/python3/dist-packages/yunohost/tests/*,/usr/lib/python3/dist-packages/yunohost/vendor/*

View file

@ -1,10 +1,11 @@
---
stages:
- lint
- build
- install
- test
- bot
- lint
- doc
- translation
default:
tags:
@ -46,7 +47,7 @@ workflow:
variables:
GIT_CLONE_PATH: '$CI_BUILDS_DIR/$CI_COMMIT_SHA/$CI_JOB_ID'
YNH_SOURCE: "https://github.com/yunohost"
YNH_DEBIAN: "bookworm"
YNH_DEBIAN: "bullseye"
YNH_SKIP_DIAGNOSIS_DURING_UPGRADE: "true"
include:

View file

@ -1,53 +0,0 @@
generate-helpers-doc:
stage: bot
image: "build-and-lint"
needs: []
before_script:
- git config --global user.email "yunohost@yunohost.org"
- git config --global user.name "$GITHUB_USER"
script:
- cd doc
- python3 generate_helper_doc.py 2
- python3 generate_helper_doc.py 2.1
- python3 generate_resource_doc.py > resources.md
- python3 generate_configpanel_and_formoptions_doc.py > forms.md
- hub clone https://$GITHUB_TOKEN:x-oauth-basic@github.com/YunoHost/doc.git doc_repo
- cp helpers.v2.md doc_repo/pages/06.contribute/10.packaging_apps/20.scripts/10.helpers/packaging_app_scripts_helpers.md
- cp helpers.v2.1.md doc_repo/pages/06.contribute/10.packaging_apps/20.scripts/12.helpers21/packaging_app_scripts_helpers_v21.md
- cp resources.md doc_repo/pages/06.contribute/10.packaging_apps/10.manifest/10.appresources/packaging_app_manifest_resources.md
- cp forms doc_repo/pages/06.contribute/15.dev/03.forms/forms.md
- cd doc_repo
# replace ${CI_COMMIT_REF_NAME} with ${CI_COMMIT_TAG} ?
- hub checkout -b "${CI_COMMIT_REF_NAME}"
- hub commit -am "[CI] Update app helpers/resources for ${CI_COMMIT_REF_NAME}"
- hub pull-request -m "[CI] Update app helpers/resources for ${CI_COMMIT_REF_NAME}" -p # GITHUB_USER and GITHUB_TOKEN registered here https://gitlab.com/yunohost/yunohost/-/settings/ci_cd
artifacts:
paths:
- doc/helpers.md
- doc/resources.md
only:
- tags
autofix-translated-strings:
stage: bot
image: "build-and-lint"
needs: []
before_script:
- git config --global user.email "yunohost@yunohost.org"
- git config --global user.name "$GITHUB_USER"
- hub clone --branch ${CI_COMMIT_REF_NAME} "https://$GITHUB_TOKEN:x-oauth-basic@github.com/YunoHost/yunohost.git" github_repo
- cd github_repo
script:
# create a local branch that will overwrite distant one
- git checkout -b "ci-autofix-translated-strings-${CI_COMMIT_REF_NAME}" --no-track
- python3 maintenance/missing_i18n_keys.py --fix
- python3 maintenance/autofix_locale_format.py
- '[ $(git diff --ignore-blank-lines --ignore-all-space --ignore-space-at-eol --ignore-cr-at-eol | wc -l) != 0 ] || exit 0' # stop if there is nothing to commit
- git commit -am "[CI] Reformat / remove stale translated strings" || true
- git push -f origin "ci-autofix-translated-strings-${CI_COMMIT_REF_NAME}":"ci-remove-stale-translated-strings-${CI_COMMIT_REF_NAME}"
- hub pull-request -m "[CI] Reformat / remove stale translated strings" -b Yunohost:$CI_COMMIT_REF_NAME -p || true # GITHUB_USER and GITHUB_TOKEN registered here https://gitlab.com/yunohost/yunohost/-/settings/ci_cd
only:
variables:
- $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH
changes:
- locales/*

View file

@ -1,14 +1,9 @@
.build-stage:
stage: build
needs:
- job: actionsmap
- job: invalidcode311
image: "build-and-lint"
variables:
YNH_BUILD_DIR: "$GIT_CLONE_PATH/build"
before_script:
- echo $PWD
- echo $CI_PROJECT_DIR
- mkdir -p $YNH_BUILD_DIR
artifacts:
paths:
@ -36,7 +31,7 @@ build-yunohost:
- mkdir -p $YNH_BUILD_DIR/$PACKAGE
- cat archive.tar.gz | tar -xz -C $YNH_BUILD_DIR/$PACKAGE
- rm archive.tar.gz
- DEBIAN_FRONTEND=noninteractive apt --assume-yes -o Dpkg::Options::="--force-confold" build-dep $YNH_BUILD_DIR/$PACKAGE || { apt-get update && DEBIAN_FRONTEND=noninteractive apt --assume-yes -o Dpkg::Options::="--force-confold" build-dep $YNH_BUILD_DIR/$PACKAGE; }
- DEBIAN_FRONTEND=noninteractive apt --assume-yes -o Dpkg::Options::="--force-confold" build-dep $YNH_BUILD_DIR/$PACKAGE
- *build_script
build-ssowat:
@ -45,7 +40,7 @@ build-ssowat:
PACKAGE: "ssowat"
script:
- git clone $YNH_SOURCE/$PACKAGE -b $CI_COMMIT_REF_NAME $YNH_BUILD_DIR/$PACKAGE --depth 1 || git clone $YNH_SOURCE/$PACKAGE -b $YNH_DEBIAN $YNH_BUILD_DIR/$PACKAGE --depth 1 || git clone $YNH_SOURCE/$PACKAGE $YNH_BUILD_DIR/$PACKAGE --depth 1
- DEBIAN_FRONTEND=noninteractive apt --assume-yes -o Dpkg::Options::="--force-confold" build-dep $YNH_BUILD_DIR/$PACKAGE || { apt-get update && DEBIAN_FRONTEND=noninteractive apt --assume-yes -o Dpkg::Options::="--force-confold" build-dep $YNH_BUILD_DIR/$PACKAGE; }
- DEBIAN_FRONTEND=noninteractive apt --assume-yes -o Dpkg::Options::="--force-confold" build-dep $YNH_BUILD_DIR/$PACKAGE
- *build_script
build-moulinette:
@ -54,5 +49,5 @@ build-moulinette:
PACKAGE: "moulinette"
script:
- git clone $YNH_SOURCE/$PACKAGE -b $CI_COMMIT_REF_NAME $YNH_BUILD_DIR/$PACKAGE --depth 1 || git clone $YNH_SOURCE/$PACKAGE -b $YNH_DEBIAN $YNH_BUILD_DIR/$PACKAGE --depth 1 || git clone $YNH_SOURCE/$PACKAGE $YNH_BUILD_DIR/$PACKAGE --depth 1
- DEBIAN_FRONTEND=noninteractive apt --assume-yes -o Dpkg::Options::="--force-confold" build-dep $YNH_BUILD_DIR/$PACKAGE || { apt-get update && DEBIAN_FRONTEND=noninteractive apt --assume-yes -o Dpkg::Options::="--force-confold" build-dep $YNH_BUILD_DIR/$PACKAGE; }
- DEBIAN_FRONTEND=noninteractive apt --assume-yes -o Dpkg::Options::="--force-confold" build-dep $YNH_BUILD_DIR/$PACKAGE
- *build_script

View file

@ -0,0 +1,31 @@
########################################
# DOC
########################################
generate-helpers-doc:
stage: doc
image: "build-and-lint"
needs: []
before_script:
- git config --global user.email "yunohost@yunohost.org"
- git config --global user.name "$GITHUB_USER"
script:
- cd doc
- python3 generate_helper_doc.py 2
- python3 generate_helper_doc.py 2.1
- python3 generate_resource_doc.py > resources.md
- hub clone https://$GITHUB_TOKEN:x-oauth-basic@github.com/YunoHost/doc.git doc_repo
- cp helpers.v2.md doc_repo/pages/06.contribute/10.packaging_apps/20.scripts/10.helpers/packaging_app_scripts_helpers.md
- cp helpers.v2.1.md doc_repo/pages/06.contribute/10.packaging_apps/20.scripts/12.helpers21/packaging_app_scripts_helpers_v21.md
- cp resources.md doc_repo/pages/06.contribute/10.packaging_apps/10.manifest/10.appresources/packaging_app_manifest_resources.md
- cd doc_repo
# replace ${CI_COMMIT_REF_NAME} with ${CI_COMMIT_TAG} ?
- hub checkout -b "${CI_COMMIT_REF_NAME}"
- hub commit -am "[CI] Update app helpers/resources for ${CI_COMMIT_REF_NAME}"
- hub pull-request -m "[CI] Update app helpers/resources for ${CI_COMMIT_REF_NAME}" -p # GITHUB_USER and GITHUB_TOKEN registered here https://gitlab.com/yunohost/yunohost/-/settings/ci_cd
artifacts:
paths:
- doc/helpers.md
- doc/resources.md
only:
- tags

View file

@ -3,39 +3,24 @@
########################################
# later we must fix lint and format-check jobs and remove "allow_failure"
actionsmap:
stage: lint
image: "build-and-lint"
needs: []
script:
- python3 -c 'import yaml; yaml.safe_load(open("share/actionsmap.yml"))'
- python3 -c 'import yaml; yaml.safe_load(open("share/actionsmap-portal.yml"))'
lint311:
lint39:
stage: lint
image: "build-and-lint"
needs: []
allow_failure: true
script:
- tox -e py311-lint
- tox -e py39-lint
invalidcode311:
invalidcode39:
stage: lint
image: "build-and-lint"
needs: []
script:
- tox -e py311-invalidcode
- tox -e py39-invalidcode
mypy:
stage: lint
image: "build-and-lint"
needs: []
script:
- tox -e py311-mypy
i18n-keys:
stage: lint
image: "build-and-lint"
needs: []
script:
- python3 maintenance/missing_i18n_keys.py --check
- tox -e py39-mypy

View file

@ -5,11 +5,13 @@
stage: test
image: "core-tests"
variables:
PYTEST_ADDOPTS: "--color=yes --cov=src"
COVERAGE_FILE: ".coverage_$CI_JOB_NAME"
PYTEST_ADDOPTS: "--color=yes"
before_script:
- *install_debs
- ln -s src yunohost
cache:
paths:
- src/tests/apps
key: "$CI_JOB_STAGE-$CI_COMMIT_REF_SLUG"
needs:
- job: build-yunohost
artifacts: true
@ -18,14 +20,42 @@
- job: build-moulinette
artifacts: true
- job: upgrade
artifacts:
paths:
- ./.coverage_*
########################################
# TESTS
########################################
full-tests:
stage: test
image: "before-install"
variables:
PYTEST_ADDOPTS: "--color=yes"
before_script:
- *install_debs
- pip install mock pip pyOpenSSL pytest pytest-cov pytest-mock pytest-sugar requests-mock "packaging<22"
- yunohost tools postinstall -d domain.tld -u syssa -F 'Syssa Mine' -p the_password --ignore-dyndns --force-diskspace
script:
- python3 -m pytest --cov=yunohost tests/ src/tests/ --junitxml=report.xml
needs:
- job: build-yunohost
artifacts: true
- job: build-ssowat
artifacts: true
- job: build-moulinette
artifacts: true
coverage: '/TOTAL.*\s+(\d+%)/'
artifacts:
reports:
junit: report.xml
test-actionmap:
extends: .test-stage
script:
- python3 -m pytest tests/test_actionmap.py
only:
changes:
- share/actionsmap.yml
test-helpers2:
extends: .test-stage
script:
@ -42,130 +72,129 @@ test-domains:
extends: .test-stage
script:
- python3 -m pytest src/tests/test_domains.py
only:
changes:
- src/domain.py
test-dns:
extends: .test-stage
script:
- python3 -m pytest src/tests/test_dns.py
only:
changes:
- src/dns.py
- src/utils/dns.py
test-apps:
extends: .test-stage
script:
- python3 -m pytest src/tests/test_apps.py
only:
changes:
- src/app.py
test-appscatalog:
extends: .test-stage
script:
- python3 -m pytest src/tests/test_app_catalog.py
only:
changes:
- src/app_calalog.py
test-appurl:
extends: .test-stage
script:
- python3 -m pytest src/tests/test_appurl.py
only:
changes:
- src/app.py
test-questions:
extends: .test-stage
script:
- python3 -m pytest src/tests/test_questions.py
only:
changes:
- src/utils/config.py
test-app-config:
extends: .test-stage
script:
- python3 -m pytest src/tests/test_app_config.py
only:
changes:
- src/app.py
- src/utils/config.py
test-app-resources:
extends: .test-stage
script:
- python3 -m pytest src/tests/test_app_resources.py
only:
changes:
- src/app.py
- src/utils/resources.py
test-changeurl:
extends: .test-stage
script:
- python3 -m pytest src/tests/test_changeurl.py
only:
changes:
- src/app.py
test-backuprestore:
extends: .test-stage
script:
- python3 -m pytest src/tests/test_backuprestore.py
only:
changes:
- src/backup.py
test-permission:
extends: .test-stage
script:
- python3 -m pytest src/tests/test_permission.py
only:
changes:
- src/permission.py
test-settings:
extends: .test-stage
script:
- python3 -m pytest src/tests/test_settings.py
only:
changes:
- src/settings.py
test-user-group:
extends: .test-stage
script:
- python3 -m pytest src/tests/test_user-group.py
only:
changes:
- src/user.py
test-regenconf:
extends: .test-stage
script:
- python3 -m pytest src/tests/test_regenconf.py
only:
changes:
- src/regenconf.py
test-service:
extends: .test-stage
script:
- python3 -m pytest src/tests/test_service.py
only:
changes:
- src/service.py
test-ldapauth:
extends: .test-stage
script:
- python3 -m pytest src/tests/test_ldapauth.py
test-sso-and-portalapi:
extends: .test-stage
script:
- python3 -m pytest src/tests/test_sso_and_portalapi.py
########################################
# COVERAGE REPORT
########################################
coverage:
stage: test
image: "core-tests"
needs:
# Yeah ... gotta list all of those individually ... https://gitlab.com/gitlab-org/gitlab/-/issues/332326
- job: test-domains
artifacts: true
- job: test-dns
artifacts: true
- job: test-apps
artifacts: true
- job: test-appscatalog
artifacts: true
- job: test-appurl
artifacts: true
- job: test-questions
artifacts: true
- job: test-app-config
artifacts: true
- job: test-app-resources
artifacts: true
- job: test-changeurl
artifacts: true
- job: test-backuprestore
artifacts: true
- job: test-permission
artifacts: true
- job: test-settings
artifacts: true
- job: test-user-group
artifacts: true
- job: test-regenconf
artifacts: true
- job: test-service
artifacts: true
- job: test-ldapauth
artifacts: true
- job: test-sso-and-portalapi
artifacts: true
script:
- coverage combine ./.coverage_*
- coverage report
only:
changes:
- src/authenticators/*.py

View file

@ -0,0 +1,36 @@
########################################
# TRANSLATION
########################################
test-i18n-keys:
stage: translation
script:
- python3 maintenance/missing_i18n_keys.py --check
only:
changes:
- locales/en.json
- src/*.py
- src/diagnosers/*.py
autofix-translated-strings:
stage: translation
image: "build-and-lint"
needs: []
before_script:
- git config --global user.email "yunohost@yunohost.org"
- git config --global user.name "$GITHUB_USER"
- hub clone --branch ${CI_COMMIT_REF_NAME} "https://$GITHUB_TOKEN:x-oauth-basic@github.com/YunoHost/yunohost.git" github_repo
- cd github_repo
script:
# create a local branch that will overwrite distant one
- git checkout -b "ci-autofix-translated-strings-${CI_COMMIT_REF_NAME}" --no-track
- python3 maintenance/missing_i18n_keys.py --fix
- python3 maintenance/autofix_locale_format.py
- '[ $(git diff --ignore-blank-lines --ignore-all-space --ignore-space-at-eol --ignore-cr-at-eol | wc -l) != 0 ] || exit 0' # stop if there is nothing to commit
- git commit -am "[CI] Reformat / remove stale translated strings" || true
- git push -f origin "ci-autofix-translated-strings-${CI_COMMIT_REF_NAME}":"ci-remove-stale-translated-strings-${CI_COMMIT_REF_NAME}"
- hub pull-request -m "[CI] Reformat / remove stale translated strings" -b Yunohost:$CI_COMMIT_REF_NAME -p || true # GITHUB_USER and GITHUB_TOKEN registered here https://gitlab.com/yunohost/yunohost/-/settings/ci_cd
only:
variables:
- $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH
changes:
- locales/*

View file

@ -1,53 +0,0 @@
#! /usr/bin/python3
# -*- coding: utf-8 -*-
import argparse
import yunohost
# Default server configuration
DEFAULT_HOST = "localhost"
DEFAULT_PORT = 6788
def _parse_api_args():
"""Parse main arguments for the api"""
parser = argparse.ArgumentParser(
add_help=False,
description="Run the YunoHost API to manage your server.",
)
srv_group = parser.add_argument_group("server configuration")
srv_group.add_argument(
"-h",
"--host",
action="store",
default=DEFAULT_HOST,
help="Host to listen on (default: %s)" % DEFAULT_HOST,
)
srv_group.add_argument(
"-p",
"--port",
action="store",
default=DEFAULT_PORT,
type=int,
help="Port to listen on (default: %d)" % DEFAULT_PORT,
)
glob_group = parser.add_argument_group("global arguments")
glob_group.add_argument(
"--debug",
action="store_true",
default=False,
help="Set log level to DEBUG",
)
glob_group.add_argument(
"--help",
action="help",
help="Show this help message and exit",
)
return parser.parse_args()
if __name__ == "__main__":
opts = _parse_api_args()
# Run the server
yunohost.portalapi(debug=opts.debug, host=opts.host, port=opts.port)

View file

@ -132,8 +132,12 @@ def main() -> bool:
)
continue
# Broadcast IPv4 and IPv6
ips: List[str] = interfaces[interface]["ipv4"] + interfaces[interface]["ipv6"]
# Only broadcast IPv4 because IPv6 is buggy ... because we ain't using python3-ifaddr >= 0.1.7
# Buster only ships 0.1.6
# Bullseye ships 0.1.7
# To be re-enabled once we're on bullseye...
# ips: List[str] = interfaces[interface]["ipv4"] + interfaces[interface]["ipv6"]
ips: List[str] = interfaces[interface]["ipv4"]
# If at least one IP is listed
if not ips:

View file

@ -1,9 +1,13 @@
{% set interfaces_list = interfaces.split(' ') %}
{% for interface in interfaces_list %}
interface-name={{ domain }},{{ interface }}
interface-name=xmpp-upload.{{ domain }},{{ interface }}
{% endfor %}
{% if ipv6 %}
host-record={{ domain }},{{ ipv6 }}
host-record=xmpp-upload.{{ domain }},{{ ipv6 }}
{% endif %}
txt-record={{ domain }},"v=spf1 mx a -all"
mx-host={{ domain }},{{ domain }},5
srv-host=_xmpp-client._tcp.{{ domain }},{{ domain }},5222,0,5
srv-host=_xmpp-server._tcp.{{ domain }},{{ domain }},5269,0,5

View file

@ -13,8 +13,9 @@ protocols = imap sieve {% if pop3_enabled == "True" %}pop3{% endif %}
mail_plugins = $mail_plugins quota notify push_notification
###############################################################################
# generated 2023-06-13, Mozilla Guideline v5.7, Dovecot 2.3.19, OpenSSL 3.0.9, intermediate configuration
# https://ssl-config.mozilla.org/#server=dovecot&version=2.3.19&config=intermediate&openssl=3.0.9&guideline=5.7
# generated 2020-08-18, Mozilla Guideline v5.6, Dovecot 2.3.4, OpenSSL 1.1.1d, intermediate configuration
# https://ssl-config.mozilla.org/#server=dovecot&version=2.3.4&config=intermediate&openssl=1.1.1d&guideline=5.6
ssl = required
@ -31,7 +32,7 @@ ssl_dh = </usr/share/yunohost/ffdhe2048.pem
# intermediate configuration
ssl_min_protocol = TLSv1.2
ssl_cipher_list = ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305
ssl_cipher_list = ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
ssl_prefer_server_ciphers = no
###############################################################################
@ -141,6 +142,18 @@ plugin {
sieve_before = /etc/dovecot/global_script/
}
plugin {
antispam_debug_target = syslog
antispam_verbose_debug = 0
antispam_backend = pipe
antispam_spam_pattern_ignorecase = junk;spam
antispam_trash_pattern_ignorecase = trash;papierkorb;deleted messages
antispam_pipe_program = /usr/bin/rspamc
antispam_pipe_program_args = -h;localhost:11334;-P;q1
antispam_pipe_program_spam_arg = learn_spam
antispam_pipe_program_notspam_arg = learn_ham
}
plugin {
quota = maildir:User quota
quota_rule2 = SPAM:ignore

View file

@ -18,7 +18,7 @@
# See man 5 jail.conf for details.
#
# [DEFAULT]
# bantime = 1h
# bantime = 3600
#
# [sshd]
# enabled = true
@ -44,52 +44,10 @@ before = paths-debian.conf
# MISCELLANEOUS OPTIONS
#
# "bantime.increment" allows to use database for searching of previously banned ip's to increase a
# default ban time using special formula, default it is banTime * 1, 2, 4, 8, 16, 32...
#bantime.increment = true
# "bantime.rndtime" is the max number of seconds using for mixing with random time
# to prevent "clever" botnets calculate exact time IP can be unbanned again:
#bantime.rndtime =
# "bantime.maxtime" is the max number of seconds using the ban time can reach (doesn't grow further)
#bantime.maxtime =
# "bantime.factor" is a coefficient to calculate exponent growing of the formula or common multiplier,
# default value of factor is 1 and with default value of formula, the ban time
# grows by 1, 2, 4, 8, 16 ...
#bantime.factor = 1
# "bantime.formula" used by default to calculate next value of ban time, default value below,
# the same ban time growing will be reached by multipliers 1, 2, 4, 8, 16, 32...
#bantime.formula = ban.Time * (1<<(ban.Count if ban.Count<20 else 20)) * banFactor
#
# more aggressive example of formula has the same values only for factor "2.0 / 2.885385" :
#bantime.formula = ban.Time * math.exp(float(ban.Count+1)*banFactor)/math.exp(1*banFactor)
# "bantime.multipliers" used to calculate next value of ban time instead of formula, corresponding
# previously ban count and given "bantime.factor" (for multipliers default is 1);
# following example grows ban time by 1, 2, 4, 8, 16 ... and if last ban count greater as multipliers count,
# always used last multiplier (64 in example), for factor '1' and original ban time 600 - 10.6 hours
#bantime.multipliers = 1 2 4 8 16 32 64
# following example can be used for small initial ban time (bantime=60) - it grows more aggressive at begin,
# for bantime=60 the multipliers are minutes and equal: 1 min, 5 min, 30 min, 1 hour, 5 hour, 12 hour, 1 day, 2 day
#bantime.multipliers = 1 5 30 60 300 720 1440 2880
# "bantime.overalljails" (if true) specifies the search of IP in the database will be executed
# cross over all jails, if false (default), only current jail of the ban IP will be searched
#bantime.overalljails = false
# --------------------
# "ignoreself" specifies whether the local resp. own IP addresses should be ignored
# (default is true). Fail2ban will not ban a host which matches such addresses.
#ignoreself = true
# "ignoreip" can be a list of IP addresses, CIDR masks or DNS hosts. Fail2ban
# will not ban a host which matches an address in this list. Several addresses
# can be defined using space (and/or comma) separator.
#ignoreip = 127.0.0.1/8 ::1
# "ignoreip" can be an IP address, a CIDR mask or a DNS host. Fail2ban will not
# ban a host which matches an address in this list. Several addresses can be
# defined using space (and/or comma) separator.
ignoreip = 127.0.0.1/8
# External command that will take an tagged arguments to ignore, e.g. <ip>,
# and return true if the IP is to be ignored. False otherwise.
@ -98,18 +56,15 @@ before = paths-debian.conf
ignorecommand =
# "bantime" is the number of seconds that a host is banned.
bantime = 10m
bantime = 600
# A host is banned if it has generated "maxretry" during the last "findtime"
# seconds.
findtime = 10m
findtime = 600
# "maxretry" is the number of failures before a host get banned.
maxretry = 10
# "maxmatches" is the number of matches stored in ticket (resolvable via tag <matches> in actions).
maxmatches = %(maxretry)s
# "backend" specifies the backend used to get files modification.
# Available options are "pyinotify", "gamin", "polling", "systemd" and "auto".
# This option can be overridden in each jail as well.
@ -158,13 +113,10 @@ logencoding = auto
enabled = false
# "mode" defines the mode of the filter (see corresponding filter implementation for more info).
mode = normal
# "filter" defines the filter to use by the jail.
# By default jails have names matching their filter name
#
filter = %(__name__)s[mode=%(mode)s]
filter = %(__name__)s
#
@ -188,7 +140,7 @@ mta = sendmail
# Default protocol
protocol = tcp
# Specify chain where jumps would need to be added in ban-actions expecting parameter chain
# Specify chain where jumps would need to be added in iptables-* actions
chain = INPUT
# Ports to be banned
@ -209,53 +161,51 @@ banaction = iptables-multiport
banaction_allports = iptables-allports
# The simplest action to take: ban only
action_ = %(banaction)s[port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"]
action_ = %(banaction)s[name=%(__name__)s, bantime="%(bantime)s", port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"]
# ban & send an e-mail with whois report to the destemail.
action_mw = %(action_)s
%(mta)s-whois[sender="%(sender)s", dest="%(destemail)s", protocol="%(protocol)s", chain="%(chain)s"]
action_mw = %(banaction)s[name=%(__name__)s, bantime="%(bantime)s", port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"]
%(mta)s-whois[name=%(__name__)s, sender="%(sender)s", dest="%(destemail)s", protocol="%(protocol)s", chain="%(chain)s"]
# ban & send an e-mail with whois report and relevant log lines
# to the destemail.
action_mwl = %(action_)s
%(mta)s-whois-lines[sender="%(sender)s", dest="%(destemail)s", logpath="%(logpath)s", chain="%(chain)s"]
action_mwl = %(banaction)s[name=%(__name__)s, bantime="%(bantime)s", port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"]
%(mta)s-whois-lines[name=%(__name__)s, sender="%(sender)s", dest="%(destemail)s", logpath=%(logpath)s, chain="%(chain)s"]
# See the IMPORTANT note in action.d/xarf-login-attack for when to use this action
#
# ban & send a xarf e-mail to abuse contact of IP address and include relevant log lines
# to the destemail.
action_xarf = %(action_)s
xarf-login-attack[service=%(__name__)s, sender="%(sender)s", logpath="%(logpath)s", port="%(port)s"]
# ban & send a notification to one or more of the 50+ services supported by Apprise.
# See https://github.com/caronc/apprise/wiki for details on what is supported.
#
# You may optionally over-ride the default configuration line (containing the Apprise URLs)
# by using 'apprise[config="/alternate/path/to/apprise.cfg"]' otherwise
# /etc/fail2ban/apprise.conf is sourced for your supported notification configuration.
# action = %(action_)s
# apprise
action_xarf = %(banaction)s[name=%(__name__)s, bantime="%(bantime)s", port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"]
xarf-login-attack[service=%(__name__)s, sender="%(sender)s", logpath=%(logpath)s, port="%(port)s"]
# ban IP on CloudFlare & send an e-mail with whois report and relevant log lines
# to the destemail.
action_cf_mwl = cloudflare[cfuser="%(cfemail)s", cftoken="%(cfapikey)s"]
%(mta)s-whois-lines[sender="%(sender)s", dest="%(destemail)s", logpath="%(logpath)s", chain="%(chain)s"]
%(mta)s-whois-lines[name=%(__name__)s, sender="%(sender)s", dest="%(destemail)s", logpath=%(logpath)s, chain="%(chain)s"]
# Report block via blocklist.de fail2ban reporting service API
#
# See the IMPORTANT note in action.d/blocklist_de.conf for when to use this action.
# Specify expected parameters in file action.d/blocklist_de.local or if the interpolation
# `action_blocklist_de` used for the action, set value of `blocklist_de_apikey`
# in your `jail.local` globally (section [DEFAULT]) or per specific jail section (resp. in
# corresponding jail.d/my-jail.local file).
# See the IMPORTANT note in action.d/blocklist_de.conf for when to
# use this action. Create a file jail.d/blocklist_de.local containing
# [Init]
# blocklist_de_apikey = {api key from registration]
#
action_blocklist_de = blocklist_de[email="%(sender)s", service="%(__name__)s", apikey="%(blocklist_de_apikey)s", agent="%(fail2ban_agent)s"]
action_blocklist_de = blocklist_de[email="%(sender)s", service=%(filter)s, apikey="%(blocklist_de_apikey)s", agent="%(fail2ban_agent)s"]
# Report ban via abuseipdb.com.
# Report ban via badips.com, and use as blacklist
#
# See action.d/abuseipdb.conf for usage example and details.
# See BadIPsAction docstring in config/action.d/badips.py for
# documentation for this action.
#
action_abuseipdb = abuseipdb
# NOTE: This action relies on banaction being present on start and therefore
# should be last action defined for a jail.
#
action_badips = badips.py[category="%(__name__)s", banaction="%(banaction)s", agent="%(fail2ban_agent)s"]
#
# Report ban via badips.com (uses action.d/badips.conf for reporting only)
#
action_badips_report = badips[category="%(__name__)s", agent="%(fail2ban_agent)s"]
# Choose default action. To change, just override value of 'action' with the
# interpolation to the chosen action shortcut (e.g. action_mw, action_mwl, etc) in jail.local
@ -273,10 +223,15 @@ action = %(action_)s
[sshd]
# To use more aggressive sshd modes set filter parameter "mode" in jail.local:
# normal (default), ddos, extra or aggressive (combines all).
# See "tests/files/logs/sshd" or "filter.d/sshd.conf" for usage example and details.
#mode = normal
port = ssh
logpath = %(sshd_log)s
backend = %(sshd_backend)s
[sshd-ddos]
# This jail corresponds to the standard configuration in Fail2ban.
# The mail-whois action send a notification e-mail with a whois request
# in the body.
port = ssh
logpath = %(sshd_log)s
backend = %(sshd_backend)s
@ -310,7 +265,7 @@ logpath = %(apache_error_log)s
# for email addresses. The mail outputs are buffered.
port = http,https
logpath = %(apache_access_log)s
bantime = 48h
bantime = 172800
maxretry = 1
@ -346,7 +301,7 @@ maxretry = 2
port = http,https
logpath = %(apache_access_log)s
maxretry = 1
ignorecommand = %(fail2ban_confpath)s/filter.d/ignorecommands/apache-fakegooglebot <ip>
ignorecommand = %(ignorecommands_dir)s/apache-fakegooglebot <ip>
[apache-modsecurity]
@ -366,15 +321,12 @@ maxretry = 1
[openhab-auth]
filter = openhab
banaction = %(banaction_allports)s
action = iptables-allports[name=NoAuthFailures]
logpath = /opt/openhab/logs/request.log
# To use more aggressive http-auth modes set filter parameter "mode" in jail.local:
# normal (default), aggressive (combines all), auth or fallback
# See "tests/files/logs/nginx-http-auth" or "filter.d/nginx-http-auth.conf" for usage example and details.
[nginx-http-auth]
# mode = normal
port = http,https
logpath = %(nginx_error_log)s
@ -390,10 +342,8 @@ logpath = %(nginx_error_log)s
port = http,https
logpath = %(nginx_error_log)s
maxretry = 2
[nginx-bad-request]
port = http,https
logpath = %(nginx_access_log)s
# Ban attackers that try to use PHP's URL-fopen() functionality
# through GET/POST variables. - Experimental, with more than a year
@ -427,8 +377,6 @@ logpath = %(lighttpd_error_log)s
port = http,https
logpath = %(roundcube_errors_log)s
# Use following line in your jail.local if roundcube logs to journal.
#backend = %(syslog_backend)s
[openwebmail]
@ -478,13 +426,11 @@ backend = %(syslog_backend)s
port = http,https
logpath = /var/log/tomcat*/catalina.out
#logpath = /var/log/guacamole.log
[monit]
#Ban clients brute-forcing the monit gui login
port = 2812
logpath = /var/log/monit
/var/log/monit.log
[webmin-auth]
@ -567,29 +513,27 @@ logpath = %(vsftpd_log)s
# ASSP SMTP Proxy Jail
[assp]
port = smtp,465,submission
port = smtp,submission
logpath = /root/path/to/assp/logs/maillog.txt
[courier-smtp]
port = smtp,465,submission
port = smtp,submission
logpath = %(syslog_mail)s
backend = %(syslog_backend)s
[postfix]
# To use another modes set filter parameter "mode" in jail.local:
mode = more
port = smtp,465,submission
port = smtp,submission
logpath = %(postfix_log)s
backend = %(postfix_backend)s
[postfix-rbl]
filter = postfix[mode=rbl]
port = smtp,465,submission
port = smtp,submission
logpath = %(postfix_log)s
backend = %(postfix_backend)s
maxretry = 1
@ -597,17 +541,14 @@ maxretry = 1
[sendmail-auth]
port = submission,465,smtp
port = submission,smtp
logpath = %(syslog_mail)s
backend = %(syslog_backend)s
[sendmail-reject]
# To use more aggressive modes set filter parameter "mode" in jail.local:
# normal (default), extra or aggressive
# See "tests/files/logs/sendmail-reject" or "filter.d/sendmail-reject.conf" for usage example and details.
#mode = normal
port = smtp,465,submission
port = smtp,submission
logpath = %(syslog_mail)s
backend = %(syslog_backend)s
@ -615,7 +556,7 @@ backend = %(syslog_backend)s
[qmail-rbl]
filter = qmail
port = smtp,465,submission
port = smtp,submission
logpath = /service/qmail/log/main/current
@ -623,14 +564,14 @@ logpath = /service/qmail/log/main/current
# but can be set by syslog_facility in the dovecot configuration.
[dovecot]
port = pop3,pop3s,imap,imaps,submission,465,sieve
port = pop3,pop3s,imap,imaps,submission,sieve
logpath = %(dovecot_log)s
backend = %(dovecot_backend)s
[sieve]
port = smtp,465,submission
port = smtp,submission
logpath = %(dovecot_log)s
backend = %(dovecot_backend)s
@ -642,21 +583,20 @@ logpath = %(solidpop3d_log)s
[exim]
# see filter.d/exim.conf for further modes supported from filter:
#mode = normal
port = smtp,465,submission
port = smtp,submission
logpath = %(exim_main_log)s
[exim-spam]
port = smtp,465,submission
port = smtp,submission
logpath = %(exim_main_log)s
[kerio]
port = imap,smtp,imaps,465
port = imap,smtp,imaps
logpath = /opt/kerio/mailserver/store/logs/security.log
@ -667,15 +607,14 @@ logpath = /opt/kerio/mailserver/store/logs/security.log
[courier-auth]
port = smtp,465,submission,imap,imaps,pop3,pop3s
port = smtp,submission,imaps,pop3,pop3s
logpath = %(syslog_mail)s
backend = %(syslog_backend)s
[postfix-sasl]
filter = postfix[mode=auth]
port = smtp,465,submission,imap,imaps,pop3,pop3s
port = smtp,submission,imap,imaps,pop3,pop3s
# You might consider monitoring /var/log/mail.warn instead if you are
# running postfix since it would provide the same log lines at the
# "warn" level but overall at the smaller filesize.
@ -692,7 +631,7 @@ backend = %(syslog_backend)s
[squirrelmail]
port = smtp,465,submission,imap,imap2,imaps,pop3,pop3s,http,https,socks
port = smtp,submission,imap,imap2,imaps,pop3,pop3s,http,https,socks
logpath = /var/lib/squirrelmail/prefs/squirrelmail_access_log
@ -745,8 +684,8 @@ logpath = /var/log/named/security.log
[nsd]
port = 53
action_ = %(default/action_)s[name=%(__name__)s-tcp, protocol="tcp"]
%(default/action_)s[name=%(__name__)s-udp, protocol="udp"]
action = %(banaction)s[name=%(__name__)s-tcp, port="%(port)s", protocol="tcp", chain="%(chain)s", actname=%(banaction)s-tcp]
%(banaction)s[name=%(__name__)s-udp, port="%(port)s", protocol="udp", chain="%(chain)s", actname=%(banaction)s-udp]
logpath = /var/log/nsd.log
@ -757,8 +696,9 @@ logpath = /var/log/nsd.log
[asterisk]
port = 5060,5061
action_ = %(default/action_)s[name=%(__name__)s-tcp, protocol="tcp"]
%(default/action_)s[name=%(__name__)s-udp, protocol="udp"]
action = %(banaction)s[name=%(__name__)s-tcp, port="%(port)s", protocol="tcp", chain="%(chain)s", actname=%(banaction)s-tcp]
%(banaction)s[name=%(__name__)s-udp, port="%(port)s", protocol="udp", chain="%(chain)s", actname=%(banaction)s-udp]
%(mta)s-whois[name=%(__name__)s, dest="%(destemail)s"]
logpath = /var/log/asterisk/messages
maxretry = 10
@ -766,22 +706,16 @@ maxretry = 10
[freeswitch]
port = 5060,5061
action_ = %(default/action_)s[name=%(__name__)s-tcp, protocol="tcp"]
%(default/action_)s[name=%(__name__)s-udp, protocol="udp"]
action = %(banaction)s[name=%(__name__)s-tcp, port="%(port)s", protocol="tcp", chain="%(chain)s", actname=%(banaction)s-tcp]
%(banaction)s[name=%(__name__)s-udp, port="%(port)s", protocol="udp", chain="%(chain)s", actname=%(banaction)s-udp]
%(mta)s-whois[name=%(__name__)s, dest="%(destemail)s"]
logpath = /var/log/freeswitch.log
maxretry = 10
# enable adminlog; it will log to a file inside znc's directory by default.
[znc-adminlog]
port = 6667
logpath = /var/lib/znc/moddata/adminlog/znc.log
# To log wrong MySQL access attempts add to /etc/my.cnf in [mysqld] or
# equivalent section:
# log-warnings = 2
# log-warning = 2
#
# for syslog (daemon facility)
# [mysqld_safe]
@ -797,14 +731,6 @@ logpath = %(mysql_log)s
backend = %(mysql_backend)s
[mssql-auth]
# Default configuration for Microsoft SQL Server for Linux
# See the 'mssql-conf' manpage how to change logpath or port
logpath = /var/opt/mssql/log/errorlog
port = 1433
filter = mssql-auth
# Log wrong MongoDB auth (for details see filter 'filter.d/mongodb-auth.conf')
[mongodb-auth]
# change port when running with "--shardsvr" or "--configsvr" runtime operation
@ -823,8 +749,8 @@ logpath = /var/log/mongodb/mongodb.log
logpath = /var/log/fail2ban.log
banaction = %(banaction_allports)s
bantime = 1w
findtime = 1d
bantime = 604800 ; 1 week
findtime = 86400 ; 1 day
# Generic filter for PAM. Has to be used with action which bans all
@ -860,31 +786,11 @@ logpath = /var/log/ejabberd/ejabberd.log
[counter-strike]
logpath = /opt/cstrike/logs/L[0-9]*.log
# Firewall: http://www.cstrike-planet.com/faq/6
tcpport = 27030,27031,27032,27033,27034,27035,27036,27037,27038,27039
udpport = 1200,27000,27001,27002,27003,27004,27005,27006,27007,27008,27009,27010,27011,27012,27013,27014,27015
action_ = %(default/action_)s[name=%(__name__)s-tcp, port="%(tcpport)s", protocol="tcp"]
%(default/action_)s[name=%(__name__)s-udp, port="%(udpport)s", protocol="udp"]
[softethervpn]
port = 500,4500
protocol = udp
logpath = /usr/local/vpnserver/security_log/*/sec.log
[gitlab]
port = http,https
logpath = /var/log/gitlab/gitlab-rails/application.log
[grafana]
port = http,https
logpath = /var/log/grafana/grafana.log
[bitwarden]
port = http,https
logpath = /home/*/bwdata/logs/identity/Identity/log.txt
[centreon]
port = http,https
logpath = /var/log/centreon/login.log
action = %(banaction)s[name=%(__name__)s-tcp, port="%(tcpport)s", protocol="tcp", chain="%(chain)s", actname=%(banaction)s-tcp]
%(banaction)s[name=%(__name__)s-udp, port="%(udpport)s", protocol="udp", chain="%(chain)s", actname=%(banaction)s-udp]
# consider low maxretry and a long bantime
# nobody except your own Nagios server should ever probe nrpe
@ -918,9 +824,7 @@ filter = apache-pass[knocking_url="%(knocking_url)s"]
logpath = %(apache_access_log)s
blocktype = RETURN
returntype = DROP
action = %(action_)s[blocktype=%(blocktype)s, returntype=%(returntype)s,
actionstart_on_demand=false, actionrepair_on_unban=true]
bantime = 1h
bantime = 3600
maxretry = 1
findtime = 1
@ -928,8 +832,8 @@ findtime = 1
[murmur]
# AKA mumble-server
port = 64738
action_ = %(default/action_)s[name=%(__name__)s-tcp, protocol="tcp"]
%(default/action_)s[name=%(__name__)s-udp, protocol="udp"]
action = %(banaction)s[name=%(__name__)s-tcp, port="%(port)s", protocol=tcp, chain="%(chain)s", actname=%(banaction)s-tcp]
%(banaction)s[name=%(__name__)s-udp, port="%(port)s", protocol=udp, chain="%(chain)s", actname=%(banaction)s-udp]
logpath = /var/log/mumble-server/mumble-server.log
@ -947,34 +851,5 @@ logpath = /var/log/haproxy.log
[slapd]
port = ldap,ldaps
filter = slapd
logpath = /var/log/slapd.log
[domino-smtp]
port = smtp,ssmtp
logpath = /home/domino01/data/IBM_TECHNICAL_SUPPORT/console.log
[phpmyadmin-syslog]
port = http,https
logpath = %(syslog_authpriv)s
backend = %(syslog_backend)s
[zoneminder]
# Zoneminder HTTP/HTTPS web interface auth
# Logs auth failures to apache2 error log
port = http,https
logpath = %(apache_error_log)s
[traefik-auth]
# to use 'traefik-auth' filter you have to configure your Traefik instance,
# see `filter.d/traefik-auth.conf` for details and service example.
port = http,https
logpath = /var/log/traefik/access.log
[scanlogd]
logpath = %(syslog_local0)s
banaction = %(banaction_allports)s
[monitorix]
port = 8080
logpath = /var/log/monitorix-httpd

View file

@ -31,12 +31,3 @@ protocol = tcp
filter = yunohost
logpath = /var/log/nginx/*error.log
/var/log/nginx/*access.log
[yunohost-portal]
enabled = true
port = http,https
protocol = tcp
filter = yunohost-portal
logpath = /var/log/nginx/*error.log
/var/log/nginx/*access.log
maxretry = 20

View file

@ -1,3 +0,0 @@
[Definition]
failregex = ^<HOST> -.*\"POST /yunohost/portalapi/login HTTP/\d.\d\" 401
ignoreregex =

View file

@ -1,3 +1,24 @@
# Fail2Ban configuration file
#
# Author: Adrien Beudin
#
# $Revision: 2 $
#
[Definition]
failregex = ^<HOST> -.*\"POST /yunohost/api/login HTTP/\d.\d\" 401
# Option: failregex
# Notes.: regex to match the password failure messages in the logfile. The
# host must be matched by a group named "host". The tag "<HOST>" can
# be used for standard IP/hostname matching and is only an alias for
# (?:::f{4,6}:)?(?P<host>[\w\-.^_]+)
# Values: TEXT
#
failregex = helpers.lua:[0-9]+: authenticate\(\): Connection failed for: .*, client: <HOST>
^<HOST> -.*\"POST /yunohost/api/login HTTP/\d.\d\" 401
# Option: ignoreregex
# Notes.: regex to ignore. If this regex matches, the line is ignored.
# Values: TEXT
#
ignoreregex =

View file

@ -0,0 +1,75 @@
VirtualHost "{{ domain }}"
enable = true
ssl = {
key = "/etc/yunohost/certs/{{ domain }}/key.pem";
certificate = "/etc/yunohost/certs/{{ domain }}/crt.pem";
}
authentication = "ldap2"
ldap = {
hostname = "localhost",
user = {
basedn = "ou=users,dc=yunohost,dc=org",
filter = "(&(objectClass=posixAccount)(mail=*@{{ domain }})(permission=cn=xmpp.main,ou=permission,dc=yunohost,dc=org))",
usernamefield = "mail",
namefield = "cn",
},
}
-- Discovery items
disco_items = {
{ "muc.{{ domain }}" },
{ "pubsub.{{ domain }}" },
{ "jabber.{{ domain }}" },
{ "vjud.{{ domain }}" },
{ "xmpp-upload.{{ domain }}" },
};
-- contact_info = {
-- abuse = { "mailto:abuse@{{ domain }}", "xmpp:admin@{{ domain }}" };
-- admin = { "mailto:root@{{ domain }}", "xmpp:admin@{{ domain }}" };
-- };
------ Components ------
-- You can specify components to add hosts that provide special services,
-- like multi-user conferences, and transports.
---Set up a MUC (multi-user chat) room server
Component "muc.{{ domain }}" "muc"
name = "{{ domain }} Chatrooms"
modules_enabled = {
"muc_limits";
"muc_log";
"muc_log_mam";
"muc_log_http";
"muc_vcard";
}
muc_event_rate = 0.5
muc_burst_factor = 10
room_default_config = {
logging = true,
persistent = true
};
---Set up a PubSub server
Component "pubsub.{{ domain }}" "pubsub"
name = "{{ domain }} Publish/Subscribe"
unrestricted_node_creation = true -- Anyone can create a PubSub node (from any server)
---Set up a HTTP Upload service
Component "xmpp-upload.{{ domain }}" "http_upload"
name = "{{ domain }} Sharing Service"
http_file_path = "/var/xmpp-upload/{{ domain }}/upload"
http_external_url = "https://xmpp-upload.{{ domain }}:443"
http_file_base_path = "/upload"
http_file_size_limit = 6*1024*1024
http_file_quota = 60*1024*1024
http_upload_file_size_limit = 100 * 1024 * 1024 -- bytes
http_upload_quota = 10 * 1024 * 1024 * 1024 -- bytes
---Set up a VJUD service
Component "vjud.{{ domain }}" "vjud"
vjud_disco_name = "{{ domain }} User Directory"

View file

@ -0,0 +1,123 @@
-- ** Metronome's config file example **
--
-- The format is exactly equal to Prosody's:
--
-- Lists are written { "like", "this", "one" }
-- Lists can also be of { 1, 2, 3 } numbers, etc.
-- Either commas, or semi-colons; may be used as seperators.
--
-- A table is a list of values, except each value has a name. An
-- example would be:
--
-- 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.
-- 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.
"pubsub"; -- Publish-subscribe XEP-0060
"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"
-- HTTP server
http_ports = { 5290 }
http_interfaces = { "127.0.0.1", "::1" }
--https_ports = { 5291 }
--https_interfaces = { "127.0.0.1", "::1" }
-- Enable IPv6
use_ipv6 = true
-- BOSH configuration (mod_bosh)
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
-- Use LDAP storage backend for all stores
storage = "ldap"
-- stanza optimization
csi_config_queue_all_muc_messages_but_mentions = false;
-- 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
}
------ 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" }
----------- Virtual hosts -----------
-- You need to add a VirtualHost entry for each domain you wish Metronome to serve.
-- Settings under each VirtualHost entry apply *only* to that host.
Include "conf.d/*.cfg.lua"

View file

@ -0,0 +1,270 @@
-- vim:sts=4 sw=4
-- Prosody IM
-- Copyright (C) 2008-2010 Matthew Wild
-- Copyright (C) 2008-2010 Waqas Hussain
-- Copyright (C) 2012 Rob Hoelz
--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
local ldap;
local connection;
local params = module:get_option("ldap");
local format = string.format;
local tconcat = table.concat;
local _M = {};
local config_params = {
hostname = 'string',
user = {
basedn = 'string',
namefield = 'string',
filter = 'string',
usernamefield = 'string',
},
groups = {
basedn = 'string',
namefield = 'string',
memberfield = 'string',
_member = {
name = 'string',
admin = 'boolean?',
},
},
admin = {
_optional = true,
basedn = 'string',
namefield = 'string',
filter = 'string',
}
}
local function run_validation(params, config, prefix)
prefix = prefix or '';
-- verify that every required member of config is present in params
for k, v in pairs(config) do
if type(k) == 'string' and k:sub(1, 1) ~= '_' then
local is_optional;
if type(v) == 'table' then
is_optional = v._optional;
else
is_optional = v:sub(-1) == '?';
end
if not is_optional and params[k] == nil then
return nil, prefix .. k .. ' is required';
end
end
end
for k, v in pairs(params) do
local expected_type = config[k];
local ok, err = true;
if type(k) == 'string' then
-- verify that this key is present in config
if k:sub(1, 1) == '_' or expected_type == nil then
return nil, 'invalid parameter ' .. prefix .. k;
end
-- type validation
if type(expected_type) == 'string' then
if expected_type:sub(-1) == '?' then
expected_type = expected_type:sub(1, -2);
end
if type(v) ~= expected_type then
return nil, 'invalid type for parameter ' .. prefix .. k;
end
else -- it's a table (or had better be)
if type(v) ~= 'table' then
return nil, 'invalid type for parameter ' .. prefix .. k;
end
-- recurse into child
ok, err = run_validation(v, expected_type, prefix .. k .. '.');
end
else -- it's an integer (or had better be)
if not config._member then
return nil, 'invalid parameter ' .. prefix .. tostring(k);
end
ok, err = run_validation(v, config._member, prefix .. tostring(k) .. '.');
end
if not ok then
return ok, err;
end
end
return true;
end
local function validate_config()
if true then
return true; -- XXX for now
end
-- this is almost too clever (I mean that in a bad
-- maintainability sort of way)
--
-- basically this allows a free pass for a key in group members
-- equal to params.groups.namefield
setmetatable(config_params.groups._member, {
__index = function(_, k)
if k == params.groups.namefield then
return 'string';
end
end
});
local ok, err = run_validation(params, config_params);
setmetatable(config_params.groups._member, nil);
if ok then
-- a little extra validation that doesn't fit into
-- my recursive checker
local group_namefield = params.groups.namefield;
for i, group in ipairs(params.groups) do
if not group[group_namefield] then
return nil, format('groups.%d.%s is required', i, group_namefield);
end
end
-- fill in params.admin if you can
if not params.admin and params.groups then
local admingroup;
for _, groupconfig in ipairs(params.groups) do
if groupconfig.admin then
admingroup = groupconfig;
break;
end
end
if admingroup then
params.admin = {
basedn = params.groups.basedn,
namefield = params.groups.memberfield,
filter = group_namefield .. '=' .. admingroup[group_namefield],
};
end
end
end
return ok, err;
end
-- what to do if connection isn't available?
local function connect()
return ldap.open_simple(params.hostname, params.bind_dn, params.bind_password, params.use_tls);
end
-- this is abstracted so we can maintain persistent connections at a later time
function _M.getconnection()
return connect();
end
function _M.getparams()
return params;
end
-- XXX consider renaming this...it doesn't bind the current connection
function _M.bind(username, password)
local conn = _M.getconnection();
local filter = format('%s=%s', params.user.usernamefield, username);
if params.user.usernamefield == 'mail' then
filter = format('mail=%s@*', username);
end
if filter then
filter = _M.filter.combine_and(filter, params.user.filter);
end
local who = _M.singlematch {
attrs = params.user.usernamefield,
base = params.user.basedn,
filter = filter,
};
if who then
who = who.dn;
module:log('debug', '_M.bind - who: %s', who);
else
module:log('debug', '_M.bind - no DN found for username = %s', username);
return nil, format('no DN found for username = %s', username);
end
local conn, err = ldap.open_simple(params.hostname, who, password, params.use_tls);
if conn then
conn:close();
return true;
end
return conn, err;
end
function _M.singlematch(query)
local ld = _M.getconnection();
query.sizelimit = 1;
query.scope = 'subtree';
for dn, attribs in ld:search(query) do
attribs.dn = dn;
return attribs;
end
end
_M.filter = {};
function _M.filter.combine_and(...)
local parts = { '(&' };
local arg = { ... };
for _, filter in ipairs(arg) do
if filter:sub(1, 1) ~= '(' and filter:sub(-1) ~= ')' then
filter = '(' .. filter .. ')'
end
parts[#parts + 1] = filter;
end
parts[#parts + 1] = ')';
return tconcat(parts, '');
end
do
local ok, err;
metronome.unlock_globals();
ok, ldap = pcall(require, 'lualdap');
metronome.lock_globals();
if not ok then
module:log("error", "Failed to load the LuaLDAP library for accessing LDAP: %s", ldap);
module:log("error", "More information on install LuaLDAP can be found at http://www.keplerproject.org/lualdap");
return;
end
if not params then
module:log("error", "LDAP configuration required to use the LDAP storage module");
return;
end
ok, err = validate_config();
if not ok then
module:log("error", "LDAP configuration is invalid: %s", tostring(err));
return;
end
end
return _M;

View file

@ -0,0 +1,90 @@
-- vim:sts=4 sw=4
-- Metronome IM
-- Copyright (C) 2008-2010 Matthew Wild
-- Copyright (C) 2008-2010 Waqas Hussain
-- Copyright (C) 2012 Rob Hoelz
-- Copyright (C) 2015 YUNOHOST.ORG
--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
-- https://github.com/YunoHost/yunohost-config-metronome/blob/unstable/lib/modules/mod_auth_ldap2.lua
-- adapted to use common LDAP store on Metronome
local ldap = module:require 'ldap';
local new_sasl = require 'util.sasl'.new;
local jsplit = require 'util.jid'.split;
local log = module._log
if not ldap then
return;
end
function new_default_provider(host)
local provider = { name = "ldap2" };
log("debug", "initializing ldap2 authentication provider for host '%s'", host);
function provider.test_password(username, password)
return ldap.bind(username, password);
end
function provider.user_exists(username)
local params = ldap.getparams()
local filter = ldap.filter.combine_and(params.user.filter, params.user.usernamefield .. '=' .. username);
if params.user.usernamefield == 'mail' then
filter = ldap.filter.combine_and(params.user.filter, 'mail=' .. username .. '@*');
end
return ldap.singlematch {
base = params.user.basedn,
filter = filter,
};
end
function provider.get_password(username)
return nil, "Passwords unavailable for LDAP.";
end
function provider.set_password(username, password)
return nil, "Passwords unavailable for LDAP.";
end
function provider.create_user(username, password)
return nil, "Account creation/modification not available with LDAP.";
end
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,
order = { "plain_test" },
};
return new_sasl(module.host, testpass_authentication_profile);
end
function provider.is_admin(jid)
local admin_config = ldap.getparams().admin;
if not admin_config then
return;
end
local ld = ldap:getconnection();
local username = jsplit(jid);
local filter = ldap.filter.combine_and(admin_config.filter, admin_config.namefield .. '=' .. username);
return ldap.singlematch {
base = admin_config.basedn,
filter = filter,
};
end
return provider;
end
module:add_item("auth-provider", new_default_provider(module.host));

View file

@ -0,0 +1,86 @@
-- Prosody IM
-- Copyright (C) 2008-2010 Matthew Wild
-- Copyright (C) 2008-2010 Waqas Hussain
--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
local st = require "util.stanza";
local t_concat = table.concat;
local secure_auth_only = module:get_option("c2s_require_encryption")
or module:get_option("require_encryption")
or not(module:get_option("allow_unencrypted_plain_auth"));
local sessionmanager = require "core.sessionmanager";
local usermanager = require "core.usermanager";
local nodeprep = require "util.encodings".stringprep.nodeprep;
local resourceprep = require "util.encodings".stringprep.resourceprep;
module:add_feature("jabber:iq:auth");
module:hook("stream-features", function(event)
local origin, features = event.origin, event.features;
if secure_auth_only and not origin.secure then
-- Sorry, not offering to insecure streams!
return;
elseif not origin.username then
features:tag("auth", {xmlns='http://jabber.org/features/iq-auth'}):up();
end
end);
module:hook("stanza/iq/jabber:iq:auth:query", function(event)
local session, stanza = event.origin, event.stanza;
if session.type ~= "c2s_unauthed" then
(session.sends2s or session.send)(st.error_reply(stanza, "cancel", "service-unavailable", "Legacy authentication is only allowed for unauthenticated client connections."));
return true;
end
if secure_auth_only and not session.secure then
session.send(st.error_reply(stanza, "modify", "not-acceptable", "Encryption (SSL or TLS) is required to connect to this server"));
return true;
end
local username = stanza.tags[1]:child_with_name("username");
local password = stanza.tags[1]:child_with_name("password");
local resource = stanza.tags[1]:child_with_name("resource");
if not (username and password and resource) then
local reply = st.reply(stanza);
session.send(reply:query("jabber:iq:auth")
:tag("username"):up()
:tag("password"):up()
:tag("resource"):up());
else
username, password, resource = t_concat(username), t_concat(password), t_concat(resource);
username = nodeprep(username);
resource = resourceprep(resource)
if not (username and resource) then
session.send(st.error_reply(stanza, "modify", "bad-request"));
return true;
end
if usermanager.test_password(username, session.host, password) then
-- Authentication successful!
local success, err = sessionmanager.make_authenticated(session, username);
if success then
local err_type, err_msg;
success, err_type, err, err_msg = sessionmanager.bind_resource(session, resource);
if not success then
session.send(st.error_reply(stanza, err_type, err, err_msg));
session.username, session.type = nil, "c2s_unauthed"; -- FIXME should this be placed in sessionmanager?
return true;
elseif resource ~= session.resource then -- server changed resource, not supported by legacy auth
session.send(st.error_reply(stanza, "cancel", "conflict", "The requested resource could not be assigned to this session."));
session:close(); -- FIXME undo resource bind and auth instead of closing the session?
return true;
end
end
session.send(st.reply(stanza));
else
session.send(st.error_reply(stanza, "auth", "not-authorized"));
end
end
return true;
end);

View file

@ -0,0 +1,243 @@
-- vim:sts=4 sw=4
-- Metronome IM
-- Copyright (C) 2008-2010 Matthew Wild
-- Copyright (C) 2008-2010 Waqas Hussain
-- Copyright (C) 2012 Rob Hoelz
-- Copyright (C) 2015 YUNOHOST.ORG
--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
----------------------------------------
-- Constants and such --
----------------------------------------
local setmetatable = setmetatable;
local get_config = require "core.configmanager".get;
local ldap = module:require 'ldap';
local vcardlib = module:require 'vcard';
local st = require 'util.stanza';
local gettime = require 'socket'.gettime;
local log = module._log
if not ldap then
return;
end
local CACHE_EXPIRY = 300;
----------------------------------------
-- Utility Functions --
----------------------------------------
local function ldap_record_to_vcard(record, format)
return vcardlib.create {
record = record,
format = format,
}
end
local get_alias_for_user;
do
local user_cache;
local last_fetch_time;
local function populate_user_cache()
local user_c = get_config(module.host, 'ldap').user;
if not user_c then return; end
local ld = ldap.getconnection();
local usernamefield = user_c.usernamefield;
local namefield = user_c.namefield;
user_cache = {};
for _, attrs in ld:search { base = user_c.basedn, scope = 'onelevel', filter = user_c.filter } do
user_cache[attrs[usernamefield]] = attrs[namefield];
end
last_fetch_time = gettime();
end
function get_alias_for_user(user)
if last_fetch_time and last_fetch_time + CACHE_EXPIRY < gettime() then
user_cache = nil;
end
if not user_cache then
populate_user_cache();
end
return user_cache[user];
end
end
----------------------------------------
-- Base LDAP store class --
----------------------------------------
local function ldap_store(config)
local self = {};
local config = config;
function self:get(username)
return nil, "Data getting is not available for this storage backend";
end
function self:set(username, data)
return nil, "Data setting is not available for this storage backend";
end
return self;
end
local adapters = {};
----------------------------------------
-- Roster Storage Implementation --
----------------------------------------
adapters.roster = function (config)
-- Validate configuration requirements
if not config.groups then return nil; end
local self = ldap_store(config)
function self:get(username)
local ld = ldap.getconnection();
local contacts = {};
local memberfield = config.groups.memberfield;
local namefield = config.groups.namefield;
local filter = memberfield .. '=' .. tostring(username);
local groups = {};
for _, config in ipairs(config.groups) do
groups[ config[namefield] ] = config.name;
end
log("debug", "Found %d group(s) for user %s", select('#', groups), username)
-- XXX this kind of relies on the way we do groups at INOC
for _, attrs in ld:search { base = config.groups.basedn, scope = 'onelevel', filter = filter } do
if groups[ attrs[namefield] ] then
local members = attrs[memberfield];
for _, user in ipairs(members) do
if user ~= username then
local jid = user .. '@' .. module.host;
local record = contacts[jid];
if not record then
record = {
subscription = 'both',
groups = {},
name = get_alias_for_user(user),
};
contacts[jid] = record;
end
record.groups[ groups[ attrs[namefield] ] ] = true;
end
end
end
end
return contacts;
end
function self:set(username, data)
log("warn", "Setting data in Roster LDAP storage is not supported yet")
return nil, "not supported";
end
return self;
end
----------------------------------------
-- vCard Storage Implementation --
----------------------------------------
adapters.vcard = function (config)
-- Validate configuration requirements
if not config.vcard_format or not config.user then return nil; end
local self = ldap_store(config)
function self:get(username)
local ld = ldap.getconnection();
local filter = config.user.usernamefield .. '=' .. tostring(username);
log("debug", "Retrieving vCard for user '%s'", username);
local match = ldap.singlematch {
base = config.user.basedn,
filter = filter,
};
if match then
match.jid = username .. '@' .. module.host
return st.preserialize(ldap_record_to_vcard(match, config.vcard_format));
else
return nil, "username not found";
end
end
function self:set(username, data)
log("warn", "Setting data in vCard LDAP storage is not supported yet")
return nil, "not supported";
end
return self;
end
----------------------------------------
-- Driver Definition --
----------------------------------------
cache = {};
local driver = { name = "ldap" };
function driver:open(store)
log("debug", "Opening ldap storage backend for host '%s' and store '%s'", module.host, store);
if not cache[module.host] then
log("debug", "Caching adapters for the host '%s'", module.host);
local ad_config = get_config(module.host, "ldap");
local ad_cache = {};
for k, v in pairs(adapters) do
ad_cache[k] = v(ad_config);
end
cache[module.host] = ad_cache;
end
local adapter = cache[module.host][store];
if not adapter then
log("info", "Unavailable adapter for store '%s'", store);
return nil, "unsupported-store";
end
return adapter;
end
function driver:stores(username, type, pattern)
return nil, "not implemented";
end
function driver:store_exists(username, type)
return nil, "not implemented";
end
function driver:purge(username)
return nil, "not implemented";
end
function driver:nodes(type)
return nil, "not implemented";
end
module:add_item("data-driver", driver);

View file

@ -0,0 +1,162 @@
-- vim:sts=4 sw=4
-- Prosody IM
-- Copyright (C) 2008-2010 Matthew Wild
-- Copyright (C) 2008-2010 Waqas Hussain
-- Copyright (C) 2012 Rob Hoelz
--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
local st = require 'util.stanza';
local VCARD_NS = 'vcard-temp';
local builder_methods = {};
local base64_encode = require('util.encodings').base64.encode;
function builder_methods:addvalue(key, value)
self.vcard:tag(key):text(value):up();
end
function builder_methods:addphotofield(tagname, format_section)
local record = self.record;
local format = self.format;
local vcard = self.vcard;
local config = format[format_section];
if not config then
return;
end
if config.extval then
if record[config.extval] then
local tag = vcard:tag(tagname);
tag:tag('EXTVAL'):text(record[config.extval]):up();
end
elseif config.type and config.binval then
if record[config.binval] then
local tag = vcard:tag(tagname);
tag:tag('TYPE'):text(config.type):up();
tag:tag('BINVAL'):text(base64_encode(record[config.binval])):up();
end
else
module:log('error', 'You have an invalid %s config section', tagname);
return;
end
vcard:up();
end
function builder_methods:addregularfield(tagname, format_section)
local record = self.record;
local format = self.format;
local vcard = self.vcard;
if not format[format_section] then
return;
end
local tag = vcard:tag(tagname);
for k, v in pairs(format[format_section]) do
tag:tag(string.upper(k)):text(record[v]):up();
end
vcard:up();
end
function builder_methods:addmultisectionedfield(tagname, format_section)
local record = self.record;
local format = self.format;
local vcard = self.vcard;
if not format[format_section] then
return;
end
for k, v in pairs(format[format_section]) do
local tag = vcard:tag(tagname);
if type(k) == 'string' then
tag:tag(string.upper(k)):up();
end
for k2, v2 in pairs(v) do
if type(v2) == 'boolean' then
tag:tag(string.upper(k2)):up();
else
tag:tag(string.upper(k2)):text(record[v2]):up();
end
end
vcard:up();
end
end
function builder_methods:build()
local record = self.record;
local format = self.format;
self:addvalue( 'VERSION', '2.0');
self:addvalue( 'FN', record[format.displayname]);
self:addregularfield( 'N', 'name');
self:addvalue( 'NICKNAME', record[format.nickname]);
self:addphotofield( 'PHOTO', 'photo');
self:addvalue( 'BDAY', record[format.birthday]);
self:addmultisectionedfield('ADR', 'address');
self:addvalue( 'LABEL', nil); -- we don't support LABEL...yet.
self:addmultisectionedfield('TEL', 'telephone');
self:addmultisectionedfield('EMAIL', 'email');
self:addvalue( 'JABBERID', record.jid);
self:addvalue( 'MAILER', record[format.mailer]);
self:addvalue( 'TZ', record[format.timezone]);
self:addregularfield( 'GEO', 'geo');
self:addvalue( 'TITLE', record[format.title]);
self:addvalue( 'ROLE', record[format.role]);
self:addphotofield( 'LOGO', 'logo');
self:addvalue( 'AGENT', nil); -- we don't support AGENT...yet.
self:addregularfield( 'ORG', 'org');
self:addvalue( 'CATEGORIES', nil); -- we don't support CATEGORIES...yet.
self:addvalue( 'NOTE', record[format.note]);
self:addvalue( 'PRODID', nil); -- we don't support PRODID...yet.
self:addvalue( 'REV', record[format.rev]);
self:addvalue( 'SORT-STRING', record[format.sortstring]);
self:addregularfield( 'SOUND', 'sound');
self:addvalue( 'UID', record[format.uid]);
self:addvalue( 'URL', record[format.url]);
self:addvalue( 'CLASS', nil); -- we don't support CLASS...yet.
self:addregularfield( 'KEY', 'key');
self:addvalue( 'DESC', record[format.description]);
return self.vcard;
end
local function new_builder(params)
local vcard_tag = st.stanza('vCard', { xmlns = VCARD_NS });
local object = {
vcard = vcard_tag,
__index = builder_methods,
};
for k, v in pairs(params) do
object[k] = v;
end
setmetatable(object, object);
return object;
end
local _M = {};
function _M.create(params)
local builder = new_builder(params);
return builder:build();
end
return _M;

View file

@ -0,0 +1,8 @@
# Insert YunoHost button + portal overlay
sub_filter </head> '<script type="text/javascript" src="/ynh_portal.js"></script><link type="text/css" rel="stylesheet" href="/ynh_overlay.css"><script type="text/javascript" src="/ynhtheme/custom_portal.js"></script><link type="text/css" rel="stylesheet" href="/ynhtheme/custom_overlay.css"></head>';
sub_filter_once on;
# Apply to other mime types than text/html
sub_filter_types application/xhtml+xml;
# Prevent YunoHost panel files from being blocked by specific app rules
location ~ (ynh_portal.js|ynh_overlay.css|ynh_userinfo.json|ynhtheme/custom_portal.js|ynhtheme/custom_overlay.css) {
}

View file

@ -0,0 +1,7 @@
# Avoid the nginx path/alias traversal weakness ( #1037 )
rewrite ^/yunohost/sso$ /yunohost/sso/ permanent;
location /yunohost/sso/ {
# This is an empty location, only meant to avoid other locations
# from matching /yunohost/sso, such that it's correctly handled by ssowat
}

View file

@ -3,16 +3,16 @@ ssl_session_cache shared:SSL:50m; # about 200000 sessions
ssl_session_tickets off;
{% if compatibility == "modern" %}
# generated 2023-06-13, Mozilla Guideline v5.7, nginx 1.22.1, OpenSSL 3.0.9, modern configuration
# https://ssl-config.mozilla.org/#server=nginx&version=1.22.1&config=modern&openssl=3.0.9&guideline=5.7
# generated 2020-08-14, Mozilla Guideline v5.6, nginx 1.14.2, OpenSSL 1.1.1d, modern configuration
# https://ssl-config.mozilla.org/#server=nginx&version=1.14.2&config=modern&openssl=1.1.1d&guideline=5.6
ssl_protocols TLSv1.3;
ssl_prefer_server_ciphers off;
{% else %}
# Ciphers with intermediate compatibility
# generated 2023-06-13, Mozilla Guideline v5.7, nginx 1.22.1, OpenSSL 3.0.9, intermediate configuration
# https://ssl-config.mozilla.org/#server=nginx&version=1.22.1&config=intermediate&openssl=3.0.9&guideline=5.7
# generated 2020-08-14, Mozilla Guideline v5.6, nginx 1.14.2, OpenSSL 1.1.1d, intermediate configuration
# https://ssl-config.mozilla.org/#server=nginx&version=1.14.2&config=intermediate&openssl=1.1.1d&guideline=5.6
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
# Pre-defined FFDHE group (RFC 7919)

View file

@ -6,7 +6,7 @@ map $http_upgrade $connection_upgrade {
server {
listen 80;
listen [::]:80;
server_name {{ domain }};
server_name {{ domain }}{% if xmpp_enabled == "True" %} xmpp-upload.{{ domain }} muc.{{ domain }}{% endif %};
access_by_lua_file /usr/share/ssowat/access.lua;
@ -78,3 +78,48 @@ server {
access_log /var/log/nginx/{{ domain }}-access.log;
error_log /var/log/nginx/{{ domain }}-error.log;
}
{% if xmpp_enabled == "True" %}
# vhost dedicated to XMPP http_upload
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name xmpp-upload.{{ domain }};
root /dev/null;
location /upload/ {
alias /var/xmpp-upload/{{ domain }}/upload/;
# Pass all requests to metronome, except for GET and HEAD requests.
limit_except GET HEAD {
proxy_pass http://localhost:5290;
}
include proxy_params;
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'HEAD, GET, PUT, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Authorization';
add_header 'Access-Control-Allow-Credentials' 'true';
client_max_body_size 105M; # Choose a value a bit higher than the max upload configured in XMPP server
}
include /etc/nginx/conf.d/security.conf.inc;
ssl_certificate /etc/yunohost/certs/{{ domain }}/crt.pem;
ssl_certificate_key /etc/yunohost/certs/{{ domain }}/key.pem;
{% if domain_cert_ca != "selfsigned" %}
more_set_headers "Strict-Transport-Security : max-age=63072000; includeSubDomains; preload";
{% endif %}
{% if domain_cert_ca == "letsencrypt" %}
# OCSP settings
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/yunohost/certs/{{ domain }}/crt.pem;
resolver 1.1.1.1 9.9.9.9 valid=300s;
resolver_timeout 5s;
{% endif %}
access_log /var/log/nginx/xmpp-upload.{{ domain }}-access.log;
error_log /var/log/nginx/xmpp-upload.{{ domain }}-error.log;
}
{% endif %}

View file

@ -23,24 +23,3 @@ location = /yunohost/api/error/502 {
add_header Content-Type text/plain;
internal;
}
location /yunohost/portalapi/ {
proxy_read_timeout 5s;
proxy_pass http://127.0.0.1:6788/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
# Custom 502 error page
error_page 502 /yunohost/portalapi/error/502;
}
# Yunohost admin output complete 502 error page, so use only plain text.
location = /yunohost/portalapi/error/502 {
return 502 '502 - Bad Gateway';
add_header Content-Type text/plain;
internal;
}

View file

@ -1,28 +0,0 @@
# Avoid the nginx path/alias traversal weakness ( #1037 )
rewrite ^/yunohost/sso$ /yunohost/sso/ permanent;
location /yunohost/sso/ {
alias /usr/share/yunohost/portal/;
default_type text/html;
index index.html;
try_files $uri $uri/ /index.html;
location = /yunohost/sso/index.html {
etag off;
expires off;
more_set_headers "Cache-Control: no-store, no-cache, must-revalidate";
}
location /yunohost/sso/applogos/ {
alias /usr/share/yunohost/applogos/;
}
location = /yunohost/sso/customassets/custom.css {
alias /usr/share/yunohost/portal/customassets/$host.custom.css;
etag off;
expires off;
more_set_headers "Cache-Control: no-store, no-cache, must-revalidate";
}
more_set_headers "Content-Security-Policy: upgrade-insecure-requests; default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; object-src 'none'; img-src 'self' data:;";
}

View file

@ -1,31 +0,0 @@
# General daemon config
Socket inet:8891@localhost
PidFile /run/opendkim/opendkim.pid
UserID opendkim
UMask 007
AutoRestart yes
AutoRestartCount 10
AutoRestartRate 10/1h
# Logging
Syslog yes
SyslogSuccess yes
LogWhy yes
# Common signing and verification parameters. In Debian, the "From" header is
# oversigned, because it is often the identity key used by reputation systems
# and thus somewhat security sensitive.
Canonicalization relaxed/simple
Mode sv
OversignHeaders From
#On-BadSignature reject
# Key / signing table
KeyTable file:/etc/dkim/keytable
SigningTable refile:/etc/dkim/signingtable
# The trust anchor enables DNSSEC. In Debian, the trust anchor file is provided
# by the package dns-root-data.
TrustAnchorFile /usr/share/dns/root.key
#Nameservers 127.0.0.1

View file

@ -30,8 +30,8 @@ smtpd_tls_chain_files =
tls_server_sni_maps = hash:/etc/postfix/sni
{% if compatibility == "intermediate" %}
# generated 2023-06-13, Mozilla Guideline v5.7, Postfix 3.7.5, OpenSSL 3.0.9, intermediate configuration
# https://ssl-config.mozilla.org/#server=postfix&version=3.7.5&config=intermediate&openssl=3.0.9&guideline=5.7
# generated 2020-08-18, Mozilla Guideline v5.6, Postfix 3.4.14, OpenSSL 1.1.1d, intermediate configuration
# https://ssl-config.mozilla.org/#server=postfix&version=3.4.14&config=intermediate&openssl=1.1.1d&guideline=5.6
smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
smtpd_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
@ -41,10 +41,10 @@ smtpd_tls_mandatory_ciphers = medium
# not actually 1024 bits, this applies to all DHE >= 1024 bits
smtpd_tls_dh1024_param_file = /usr/share/yunohost/ffdhe2048.pem
tls_medium_cipherlist = ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305
tls_medium_cipherlist = ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
{% else %}
# generated 2023-06-13, Mozilla Guideline v5.7, Postfix 3.7.5, OpenSSL 3.0.9, modern configuration
# https://ssl-config.mozilla.org/#server=postfix&version=3.7.5&config=modern&openssl=3.0.9&guideline=5.7
# generated 2020-08-18, Mozilla Guideline v5.6, Postfix 3.4.14, OpenSSL 1.1.1d, modern configuration
# https://ssl-config.mozilla.org/#server=postfix&version=3.4.14&config=modern&openssl=1.1.1d&guideline=5.6
smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1, !TLSv1.2
smtpd_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1, !TLSv1.2
@ -182,10 +182,9 @@ smtp_header_checks = regexp:/etc/postfix/header_checks
smtp_reply_filter = pcre:/etc/postfix/smtp_reply_filter
# Rmilter
milter_mail_macros = i {mail_addr} {client_addr} {client_name} {auth_authen} {auth_type}
milter_mail_macros = i {mail_addr} {client_addr} {client_name} {auth_authen}
milter_protocol = 6
smtpd_milters = inet:localhost:8891
non_smtpd_milters = inet:localhost:8891
smtpd_milters = inet:localhost:11332
# Skip email without checking if milter has died
milter_default_action = accept

View file

@ -0,0 +1,16 @@
allow_envfrom_empty = true;
allow_hdrfrom_mismatch = false;
allow_hdrfrom_multiple = false;
allow_username_mismatch = true;
auth_only = true;
path = "/etc/dkim/$domain.$selector.key";
selector = "mail";
sign_local = true;
symbol = "DKIM_SIGNED";
try_fallback = true;
use_domain = "header";
use_esld = false;
use_redis = false;
key_prefix = "DKIM_KEYS";

View file

@ -0,0 +1,8 @@
# Metrics settings
# This define overridden options.
actions {
reject = 21;
add_header = 8;
greylist = 4;
}

View file

@ -0,0 +1,9 @@
use = ["spam-header"];
routines {
spam-header {
header = "X-Spam";
value = "Yes";
remove = 1;
}
}

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

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

4
conf/rspamd/rspamd.sieve Normal file
View file

@ -0,0 +1,4 @@
require ["fileinto"];
if header :is "X-Spam" "Yes" {
fileinto "Junk";
}

View file

@ -56,6 +56,7 @@ objectClass: groupOfNamesYnh
gidNumber: 4002
cn: all_users
permission: cn=mail.main,ou=permission,dc=yunohost,dc=org
permission: cn=xmpp.main,ou=permission,dc=yunohost,dc=org
dn: cn=visitors,ou=groups,dc=yunohost,dc=org
objectClass: posixGroup
@ -74,6 +75,17 @@ gidNumber: 5001
showTile: FALSE
authHeader: FALSE
dn: cn=xmpp.main,ou=permission,dc=yunohost,dc=org
groupPermission: cn=all_users,ou=groups,dc=yunohost,dc=org
cn: xmpp.main
objectClass: posixGroup
objectClass: permissionYnh
isProtected: TRUE
label: XMPP
gidNumber: 5002
showTile: FALSE
authHeader: FALSE
dn: cn=ssh.main,ou=permission,dc=yunohost,dc=org
cn: ssh.main
objectClass: posixGroup

View file

@ -192,7 +192,7 @@ authorityKeyIdentifier=keyid,issuer
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
subjectAltName=DNS:yunohost.org,DNS:www.yunohost.org,DNS:ns.yunohost.org
subjectAltName=DNS:yunohost.org,DNS:www.yunohost.org,DNS:ns.yunohost.org,DNS:xmpp-upload.yunohost.org
[ v3_ca ]

View file

@ -8,6 +8,11 @@ fail2ban:
log: /var/log/fail2ban.log
category: security
test_conf: fail2ban-server --test
metronome:
log: [/var/log/metronome/metronome.log,/var/log/metronome/metronome.err]
needs_exposed_ports: [5222, 5269]
category: xmpp
ignore_if_package_is_not_installed: metronome
mysql:
log: [/var/log/mysql.log,/var/log/mysql.err,/var/log/mysql/error.log]
actual_systemd_service: mariadb
@ -23,22 +28,21 @@ nginx:
# log: /var/log/php7.4-fpm.log
# test_conf: php-fpm7.4 --test
# category: web
opendkim:
category: email
test_conf: opendkim -n
postfix:
log: [/var/log/mail.log,/var/log/mail.err]
actual_systemd_service: postfix@-
needs_exposed_ports: [25, 587]
category: email
postgresql:
actual_systemd_service: 'postgresql@15-main'
actual_systemd_service: 'postgresql@13-main'
category: database
ignore_if_package_is_not_installed: postgresql-15
ignore_if_package_is_not_installed: postgresql-13
redis-server:
log: /var/log/redis/redis-server.log
category: database
ignore_if_package_is_not_installed: redis-server
rspamd:
log: /var/log/rspamd/rspamd.log
category: email
slapd:
category: database
test_conf: slapd -Tt
@ -47,9 +51,6 @@ ssh:
test_conf: sshd -t
needs_exposed_ports: [22]
category: admin
yunohost-portal-api:
log: /var/log/yunohost-portal-api.log
category: userportal
yunohost-api:
log: /var/log/yunohost/yunohost-api.log
category: admin
@ -59,6 +60,21 @@ yunohost-firewall:
category: security
yunomdns:
category: mdns
glances: null
nsswitch: null
ssl: null
yunohost: null
bind9: null
tahoe-lafs: null
memcached: null
udisks2: null
udisk-glue: null
amavis: null
postgrey: null
spamassassin: null
rmilter: null
php5-fpm: null
php7.0-fpm: null
php7.3-fpm: null
nslcd: null
avahi-daemon: null

View file

@ -1,48 +0,0 @@
[Unit]
Description=YunoHost Portal API
After=network.target
[Service]
User=ynh-portal
Group=ynh-portal
Type=simple
ExecStart=/usr/bin/yunohost-portal-api
Restart=always
RestartSec=5
TimeoutStopSec=30
# Sandboxing options to harden security
# Details for these options: https://www.freedesktop.org/software/systemd/man/systemd.exec.html
NoNewPrivileges=yes
PrivateTmp=yes
PrivateDevices=yes
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
RestrictNamespaces=yes
RestrictRealtime=yes
DevicePolicy=closed
ProtectClock=yes
ProtectHostname=yes
ProtectProc=invisible
ProtectSystem=full
ProtectControlGroups=yes
ProtectKernelModules=yes
ProtectKernelTunables=yes
LockPersonality=yes
SystemCallArchitectures=native
SystemCallFilter=~@clock @debug @module @mount @obsolete @reboot @setuid @swap @cpu-emulation @privileged
# Denying access to capabilities that should not be relevant
# Doc: https://man7.org/linux/man-pages/man7/capabilities.7.html
CapabilityBoundingSet=~CAP_RAWIO CAP_MKNOD
CapabilityBoundingSet=~CAP_AUDIT_CONTROL CAP_AUDIT_READ CAP_AUDIT_WRITE
CapabilityBoundingSet=~CAP_SYS_BOOT CAP_SYS_TIME CAP_SYS_MODULE CAP_SYS_PACCT
CapabilityBoundingSet=~CAP_LEASE CAP_LINUX_IMMUTABLE CAP_IPC_LOCK
CapabilityBoundingSet=~CAP_BLOCK_SUSPEND CAP_WAKE_ALARM
CapabilityBoundingSet=~CAP_SYS_TTY_CONFIG
CapabilityBoundingSet=~CAP_MAC_ADMIN CAP_MAC_OVERRIDE
CapabilityBoundingSet=~CAP_NET_ADMIN CAP_NET_BROADCAST CAP_NET_RAW
CapabilityBoundingSet=~CAP_SYS_ADMIN CAP_SYS_PTRACE CAP_SYSLOG
[Install]
WantedBy=multi-user.target

62
debian/changelog vendored
View file

@ -1,65 +1,3 @@
yunohost (12.0.3) testing; urgency=low
- (sync bullseye changes since 12.0.2)
- apps: magically handle yarn as a regular package instead of an 'extra' repo now that yarn's repo is in the core ([#1888](http://github.com/YunoHost/yunohost/pull/1888))
- portalapi: we don't need absolute URLs for app logos ? (This ain't working when enabling the 'show other domains apps' because of CSP) (bc93a2e07)
- portalapi: fix portal_user_intro not being sent when authenticated, hence not displayed at all (cdf443c86)
- portal/domain settings: Improve explanation about search engine (24fb87725)
- portal/domain settings: Reduce theme list because there were too many (cf change in yunohost-portal) (9973cc703)
- portal/domain settings: add proper i18n string + help for new settings (831131476, ff0388556)
- domain settings: add a title to the Email section to have a separation w.r.t. the portal settings (279f33288)
- portal: fix extra app tiles not being displayed, gotta use the perm id as key, not just the app id (credit rodinux) (603c64e34)
- portal/sso: with the public app page, fix the root of the domain not redirecting to /yunohost/sso (a6b7ba843)
- portal: allow to configure custom CSS from the domain config panel (8f636561d)
- portal: change the way the new 'public apps' page in the portal is configured: add a simple bool toggle instead of having the 'public apps page' as a default app option, which allows to still configure a default app while the portal has the public apps page (748a20d86)
- ci: fix test_permission_propagation_on_ssowat, auth header tests (656e5c75d, 9e9313067, 44920d891)
- ci: optimizations, cleanups, partial refactor because of new CI image build process (fe9a4fba5, 059818254, a9e71e88d, 7f2da0af7, 94594e5a3, d639e1c42, 4f3b9df3f, 5a6a915af, 55e7e798f, 2fe24424f, 4fc929005, fd040b864, 0bbc14f54, 2976e7bf6)
- quality: add type hints to user.py (1ba75df0e, d4f39da20, 611846aa1, efce7f9f0, fe1c04fb2)
- i18n: Translations updated for Basque, French, Galician, Greek, Indonesian, Russian, Turkish
Thanks to all contributors <3 ! (Ali Çıır, cjdw, craftrac, Emmanuel Averty, Félix Piédallu, Ivan Davydov, José M, Josué Tille, ljf, OniriCorpe, ppr, selfhoster1312, Tagada, tituspijean, xabirequejo)
-- Alexandre Aubin <alex.aubin@mailoo.org> Sat, 31 Aug 2024 19:45:00 +0200
yunohost (12.0.2) testing; urgency=low
- Cleanup redis regen conf since redis ain't installed by default anymore (7b50c4eb6)
- bullseye->bookworm: add a trick to flag the migration as done if it's still marked as pending (0503a38a7)
- Sync with main branch
Thanks to all contributors <3 ! (Kayou)
-- Alexandre Aubin <alex.aubin@mailoo.org> Thu, 01 Aug 2024 18:08:33 +0200
yunohost (12.0.1) testing; urgency=low
- The user portal and SSO system have been reworked and split into three distinct pieces
- SSOwat only handling only the SSO/ACL logic (nginx lua middleware)
- A new “portal API” (yunohost-portal-api) service delivering authentication cookies and allowing users to retrieve/update infos
- A new portal front end (yunohost-portal)
- More information on the release note on the forum
- The base system does not install Mysql/Mariadb and PHP anymore
- Rspamd (antispam system) and Metronome (XMPP server) are not part of the core anymore. Instead, they are now separate applications : rspamd_ynh and metronome_ynh
- webadmin: rework cookie/session expiration mechanism. Cookies are now still valid after restarting the API (preventing clumsy disconnect during self-upgrades) and the cookie validity is automatically extended every time an API request is performed.
- mail: DKIM email signing is now done using opendkim instead of rspamd
- various compatibility tweakings for Bookworm
- regenconf: update nginx and dovecot ciphers according to Mozilla recommendation
- regenconf: update fail2ban config
- configpanels: refactor to use pydantic for more typing and consistency, add proper autogenerated doc
- apps: Yarn third-party repo is now available by default in apt config just like Sury, no need for an extra apt resource thingy
- various legacy cleanups (more info on the release note on the forum)
- perf: minimize regen-conf calls to yunohost settings get, and other misc lazy-loading optimizations
- quality: simplify the logging mess
- quality: rework ci tests workflow
-- Alexandre Aubin <alex.aubin@mailoo.org> Fri, 26 Jul 2024 22:40:16 +0200
yunohost (12.0.0) unstable; urgency=low
- Tmp changelog to prepare Bookworm
-- Alexandre Aubin <alex.aubin@mailoo.org> Thu, 04 May 2023 20:30:19 +0200
yunohost (11.2.30) stable; urgency=low
- helpers v2.1: check if patches dir exists before getting realpath ([#1938](http://github.com/YunoHost/yunohost/pull/1938))

40
debian/control vendored
View file

@ -2,22 +2,21 @@ Source: yunohost
Section: utils
Priority: extra
Maintainer: YunoHost Contributors <contrib@yunohost.org>
Build-Depends: debhelper (>=9), debhelper-compat (= 13), dh-python, python3-all (>= 3.11), python3-yaml, python3-jinja2 (>= 3.0)
Build-Depends: debhelper (>=9), debhelper-compat (= 13), dh-python, python3-all (>= 3.7), python3-yaml, python3-jinja2
Standards-Version: 3.9.6
Homepage: https://yunohost.org/
Package: yunohost
Essential: yes
Architecture: all
Depends: python3-all (>= 3.11),
, moulinette (>= 12.0), ssowat (>= 12.0),
Depends: ${python3:Depends}, ${misc:Depends}
, moulinette (>= 11.1), moulinette (<< 12.0), ssowat (>= 11.1), ssowat (<< 12.0)
, python3-psutil, python3-requests, python3-dnspython, python3-openssl
, python3-miniupnpc, python3-dbus, python3-jinja2 (>= 3.0)
, python3-miniupnpc, python3-dbus, python3-jinja2
, python3-toml, python3-packaging, python3-publicsuffix2
, python3-ldap, python3-zeroconf (>= 0.47), python3-lexicon,
, python3-cryptography, python3-jwt, python3-passlib, python3-magic
, python-is-python3, python3-pydantic, python3-email-validator
, nginx, nginx-extras (>=1.22)
, python3-ldap, python3-zeroconf (>= 0.36), python3-lexicon,
, python-is-python3
, nginx, nginx-extras (>=1.18)
, apt, apt-transport-https, apt-utils, aptitude, dirmngr
, openssh-server, iptables, fail2ban, bind9-dnsutils
, openssl, ca-certificates, netcat-openbsd, iproute2
@ -25,26 +24,31 @@ Depends: python3-all (>= 3.11),
, dnsmasq, resolvconf, libnss-myhostname
, postfix, postfix-ldap, postfix-policyd-spf-perl, postfix-pcre
, dovecot-core, dovecot-ldap, dovecot-lmtpd, dovecot-managesieved, dovecot-antispam
, opendkim-tools, opendkim, postsrsd, procmail, mailutils
, rspamd, opendkim-tools, postsrsd, procmail, mailutils
, redis-server
, acl
, git, curl, wget, cron, unzip, jq, bc, at, procps, j2cli
, lsb-release, haveged, fake-hwclock, lsof, whois
Recommends: yunohost-admin, yunohost-portal (>= 12.0)
Recommends: yunohost-admin
, ntp, inetutils-ping | iputils-ping
, bash-completion, rsyslog
, php7.4-common, php7.4-fpm, php7.4-ldap, php7.4-intl
, mariadb-server, php7.4-mysql
, php7.4-gd, php7.4-curl, php-php-gettext
, python3-pip
, unattended-upgrades
, libdbd-ldap-perl, libnet-dns-perl
, metronome (>=3.14.0)
Conflicts: iptables-persistent
, apache2
, bind9
, openresolv
, systemd-resolved
, nginx-extras (>= 1.23)
, openssl (>= 3.1)
, slapd (>= 2.6)
, dovecot-core (>= 1:2.4)
, fail2ban (>= 1.1)
, iptables (>= 1.8.10)
, nginx-extras (>= 1.19)
, openssl (>= 3.0)
, slapd (>= 2.4.58)
, dovecot-core (>= 1:2.3.14)
, redis-server (>= 5:6.1)
, fail2ban (>= 0.11.3)
, iptables (>= 1.8.8)
Description: manageable and configured self-hosting server
YunoHost aims to make self-hosting accessible to everyone. It configures
an email, Web and IM server alongside a LDAP base. It also provides

1
debian/install vendored
View file

@ -6,4 +6,5 @@ conf/* /usr/share/yunohost/conf/
locales/* /usr/share/yunohost/locales/
doc/yunohost.8.gz /usr/share/man/man8/
doc/bash_completion.d/* /etc/bash_completion.d/
conf/metronome/modules/* /usr/lib/metronome/modules/
src/* /usr/lib/python3/dist-packages/yunohost/

6
debian/postinst vendored
View file

@ -4,10 +4,6 @@ set -e
do_configure() {
mkdir -p /etc/yunohost
mkdir -p /etc/yunohost/apps
mkdir -p /etc/yunohost/portal
if [ ! -f /etc/yunohost/installed ]; then
# If apps/ is not empty, we're probably already installed in the past and
# something funky happened ...
@ -37,8 +33,6 @@ do_configure() {
yunohost tools update apps --output-as none || true
fi
systemctl restart yunohost-portal-api
# Trick to let yunohost handle the restart of the API,
# to prevent the webadmin from cutting the branch it's sitting on
if systemctl is-enabled yunohost-api --quiet

View file

@ -1,181 +0,0 @@
import ast
import datetime
import subprocess
version = open("../debian/changelog").readlines()[0].split()[1].strip("()")
today = datetime.datetime.now().strftime("%d/%m/%Y")
def get_current_commit():
p = subprocess.Popen(
"git rev-parse --verify HEAD",
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
stdout, stderr = p.communicate()
current_commit = stdout.strip().decode("utf-8")
return current_commit
current_commit = get_current_commit()
def print_config_panel_docs():
fname = "../src/utils/configpanel.py"
content = open(fname).read()
# NB: This magic is because we want to be able to run this script outside of a YunoHost context,
# in which we cant really 'import' the file because it will trigger a bunch of moulinette/yunohost imports...
tree = ast.parse(content)
ConfigPanelClasses = reversed(
[
c
for c in tree.body
if isinstance(c, ast.ClassDef)
and c.name in {"SectionModel", "PanelModel", "ConfigPanelModel"}
]
)
print("## Configuration panel structure")
for c in ConfigPanelClasses:
doc = ast.get_docstring(c)
print("")
print(f"### {c.name.replace('Model', '')}")
print("")
print(doc)
print("")
print("---")
def print_form_doc():
fname = "../src/utils/form.py"
content = open(fname).read()
# NB: This magic is because we want to be able to run this script outside of a YunoHost context,
# in which we cant really 'import' the file because it will trigger a bunch of moulinette/yunohost imports...
tree = ast.parse(content)
OptionClasses = [
c
for c in tree.body
if isinstance(c, ast.ClassDef) and c.name.endswith("Option")
]
OptionDocString = {}
print("## List of all option types")
for c in OptionClasses:
if not isinstance(c.body[0], ast.Expr):
continue
option_type = None
if c.name in {"BaseOption", "BaseInputOption"}:
option_type = c.name
elif c.body[1].target.id == "type":
option_type = c.body[1].value.attr
generaltype = (
c.bases[0].id.replace("Option", "").replace("Base", "").lower()
if c.bases
else None
)
docstring = ast.get_docstring(c)
if docstring:
if "#### Properties" not in docstring:
docstring += """
#### Properties
- [common properties](#common-properties)"""
OptionDocString[option_type] = {
"doc": docstring,
"generaltype": generaltype,
}
# Dirty hack to have "BaseOption" as first and "BaseInputOption" as 2nd in list
base = OptionDocString.pop("BaseOption")
baseinput = OptionDocString.pop("BaseInputOption")
OptionDocString2 = {
"BaseOption": base,
"BaseInputOption": baseinput,
}
OptionDocString2.update(OptionDocString)
for option_type, infos in OptionDocString2.items():
if option_type == "display_text":
# display_text is kind of legacy x_x
continue
print("")
if option_type == "BaseOption":
print("### Common properties")
elif option_type == "BaseInputOption":
print("### Common inputs properties")
else:
print(
f"### `{option_type}`"
+ (f" ({infos['generaltype']})" if infos["generaltype"] else "")
)
print("")
print(infos["doc"])
print("")
print("---")
print(
rf"""---
title: Technical details for config panel structure and form option types
template: docs
taxonomy:
category: docs
routes:
default: '/dev/forms'
---
Doc auto-generated by [this script](https://github.com/YunoHost/yunohost/blob/{current_commit}/doc/generate_options_doc.py) on {today} (YunoHost version {version})
## Glossary
You may encounter some named types which are used for simplicity.
- `Translation`: a translated property
- used for properties: `ask`, `help` and `Pattern.error`
- a `dict` with locales as keys and translations as values:
```toml
ask.en = "The text in english"
ask.fr = "Le texte en français"
```
It is not currently possible for translators to translate those string in weblate.
- a single `str` for a single english default string
```toml
help = "The text in english"
```
- `JSExpression`: a `str` JS expression to be evaluated to `true` or `false`:
- used for properties: `visible` and `enabled`
- operators availables: `==`, `!=`, `>`, `>=`, `<`, `<=`, `!`, `&&`, `||`, `+`, `-`, `*`, `/`, `%` and `match()`
- `Binding`: bind a value to a file/property/variable/getter/setter/validator
- save the value in `settings.yaml` when not defined
- nothing at all with `"null"`
- a custom getter/setter/validator with `"null"` + a function starting with `get__`, `set__`, `validate__` in `scripts/config`
- a variable/property in a file with `:__FINALPATH__/my_file.php`
- a whole file with `__FINALPATH__/my_file.php`
- `Pattern`: a `dict` with a regex to match the value against and an error message
```toml
pattern.regexp = '^[A-F]\d\d$'
pattern.error = "Provide a room number such as F12: one uppercase and 2 numbers"
# or with translated error
pattern.error.en = "Provide a room number such as F12: one uppercase and 2 numbers"
pattern.error.fr = "Entrez un numéro de salle comme F12: une lettre majuscule et deux chiffres."
```
- IMPORTANT: your `pattern.regexp` should be between simple quote, not double.
"""
)
print_config_panel_docs()
print_form_doc()

View file

@ -1,4 +0,0 @@
from yunohost.utils.configpanel import ConfigPanelModel
print(ConfigPanelModel.schema_json(indent=2))

View file

@ -113,7 +113,7 @@ ignoreregex =
chown -R "$app:$app" "/var/log/$app"
chmod -R u=rwX,g=rX,o= "/var/log/$app"
ynh_systemd_action --service_name=fail2ban --action=reload --line_match="(Started|Reloaded) fail2ban.service" --log_path=systemd
ynh_systemd_action --service_name=fail2ban --action=reload --line_match="(Started|Reloaded) Fail2Ban Service" --log_path=systemd
local fail2ban_error="$(journalctl --no-hostname --unit=fail2ban | tail --lines=50 | grep "WARNING.*$app.*")"
if [[ -n "$fail2ban_error" ]]; then

View file

@ -227,9 +227,6 @@ ynh_mysql_setup_db() {
# If $db_pwd is not provided, use new_db_pwd instead for db_pwd
db_pwd="${db_pwd:-$new_db_pwd}"
# Dirty patch for super-legacy apps
dpkg --list | grep -q "^ii mariadb-server" || { ynh_print_warn "Packager: you called ynh_mysql_setup_db without declaring a dependency to mariadb-server. Please add it to your apt dependencies !"; ynh_apt install mariadb-server; }
ynh_mysql_create_db "$db_name" "$db_user" "$db_pwd"
ynh_app_setting_set --app=$app --key=mysqlpwd --value=$db_pwd
}

View file

@ -1,6 +1,6 @@
#!/bin/bash
readonly YNH_DEFAULT_PHP_VERSION=8.2
readonly YNH_DEFAULT_PHP_VERSION=7.4
# Declare the actual PHP version to use.
# A packager willing to use another version of PHP can override the variable into its _common.sh.
YNH_PHP_VERSION=${YNH_PHP_VERSION:-$YNH_DEFAULT_PHP_VERSION}
@ -70,14 +70,17 @@ YNH_PHP_VERSION=${YNH_PHP_VERSION:-$YNH_DEFAULT_PHP_VERSION}
ynh_add_fpm_config() {
local _globalphpversion=${phpversion-:}
# Declare an array to define the options of this helper.
local legacy_args=vufg
local -A args_array=([v]=phpversion= [u]=usage= [f]=footprint= [g]=group=)
local legacy_args=vufpdg
local -A args_array=([v]=phpversion= [u]=usage= [f]=footprint= [p]=package= [d]=dedicated_service [g]=group=)
local group
local phpversion
local usage
local footprint
local package
local dedicated_service
# Manage arguments with getopts
ynh_handle_getopts_args "$@"
package=${package:-}
group=${group:-}
# The default behaviour is to use the template.
@ -102,6 +105,8 @@ ynh_add_fpm_config() {
fi
fi
# Do not use a dedicated service by default
dedicated_service=${dedicated_service:-0}
# Set the default PHP-FPM version by default
if dpkg --compare-versions ${YNH_APP_PACKAGING_FORMAT:-0} lt 2; then
@ -124,16 +129,45 @@ ynh_add_fpm_config() {
fi
fi
# Legacy args (packager should just list their php dependency as regular apt dependencies...
if [ -n "$package" ]; then
# Install the additionnal packages from the default repository
ynh_print_warn --message "Argument --package of ynh_add_fpm_config is deprecated and to be removed in the future"
ynh_install_app_dependencies "$package"
fi
if [ $dedicated_service -eq 1 ]; then
ynh_print_warn --message "Argument --dedicated_service of ynh_add_fpm_config is deprecated and to be removed in the future"
local fpm_service="${app}-phpfpm"
local fpm_config_dir="/etc/php/$phpversion/dedicated-fpm"
else
local fpm_service="php${phpversion}-fpm"
local fpm_config_dir="/etc/php/$phpversion/fpm"
fi
# Create the directory for FPM pools
mkdir --parents "$fpm_config_dir/pool.d"
ynh_app_setting_set --app=$app --key=fpm_config_dir --value="$fpm_config_dir"
ynh_app_setting_set --app=$app --key=fpm_service --value="$fpm_service"
ynh_app_setting_set --app=$app --key=fpm_dedicated_service --value="$dedicated_service"
ynh_app_setting_set --app=$app --key=phpversion --value=$phpversion
# Migrate from mutual PHP service to dedicated one.
if [ $dedicated_service -eq 1 ]; then
local old_fpm_config_dir="/etc/php/$phpversion/fpm"
# If a config file exist in the common pool, move it.
if [ -e "$old_fpm_config_dir/pool.d/$app.conf" ]; then
ynh_print_info --message="Migrate to a dedicated php-fpm service for $app."
# Create a backup of the old file before migration
ynh_backup_if_checksum_is_different --file="$old_fpm_config_dir/pool.d/$app.conf"
# Remove the old PHP config file
ynh_secure_remove --file="$old_fpm_config_dir/pool.d/$app.conf"
# Reload PHP to release the socket and allow the dedicated service to use it
ynh_systemd_action --service_name=php${phpversion}-fpm --action=reload
fi
fi
if [ $autogenconf == "false" ]; then
# Usage 1, use the template in conf/php-fpm.conf
local phpfpm_path="$YNH_APP_BASEDIR/conf/php-fpm.conf"
@ -187,14 +221,56 @@ pm.process_idle_timeout = 10s
local finalphpconf="$fpm_config_dir/pool.d/$app.conf"
ynh_add_config --template="$phpfpm_path" --destination="$finalphpconf"
if [ -e "$YNH_APP_BASEDIR/conf/php-fpm.ini" ]; then
ynh_print_warn --message="Packagers ! Please do not use a separate php ini file, merge your directives in the pool file instead."
ynh_add_config --template="php-fpm.ini" --destination="$fpm_config_dir/conf.d/20-$app.ini"
fi
if [ $dedicated_service -eq 1 ]; then
# Create a dedicated php-fpm.conf for the service
local globalphpconf=$fpm_config_dir/php-fpm-$app.conf
echo "[global]
pid = /run/php/php__PHPVERSION__-fpm-__APP__.pid
error_log = /var/log/php/fpm-php.__APP__.log
syslog.ident = php-fpm-__APP__
include = __FINALPHPCONF__
" > $YNH_APP_BASEDIR/conf/php-fpm-$app.conf
ynh_add_config --template="php-fpm-$app.conf" --destination="$globalphpconf"
# Create a config for a dedicated PHP-FPM service for the app
echo "[Unit]
Description=PHP __PHPVERSION__ FastCGI Process Manager for __APP__
After=network.target
[Service]
Type=notify
PIDFile=/run/php/php__PHPVERSION__-fpm-__APP__.pid
ExecStart=/usr/sbin/php-fpm__PHPVERSION__ --nodaemonize --fpm-config __GLOBALPHPCONF__
ExecReload=/bin/kill -USR2 \$MAINPID
[Install]
WantedBy=multi-user.target
" > $YNH_APP_BASEDIR/conf/$fpm_service
# Create this dedicated PHP-FPM service
ynh_add_systemd_config --service=$fpm_service --template=$fpm_service
# Integrate the service in YunoHost admin panel
yunohost service add $fpm_service --log /var/log/php/fpm-php.$app.log --description "Php-fpm dedicated to $app"
# Configure log rotate
ynh_use_logrotate --logfile=/var/log/php
# Restart the service, as this service is either stopped or only for this app
ynh_systemd_action --service_name=$fpm_service --action=restart
else
# Validate that the new php conf doesn't break php-fpm entirely
if ! php-fpm${phpversion} --test 2> /dev/null; then
php-fpm${phpversion} --test || true
ynh_secure_remove --file="$finalphpconf"
ynh_die --message="The new configuration broke php-fpm?"
fi
ynh_systemd_action --service_name=$fpm_service --action=reload
fi
}
# Remove the dedicated PHP-FPM config
@ -205,6 +281,8 @@ pm.process_idle_timeout = 10s
ynh_remove_fpm_config() {
local fpm_config_dir=$(ynh_app_setting_get --app=$app --key=fpm_config_dir)
local fpm_service=$(ynh_app_setting_get --app=$app --key=fpm_service)
local dedicated_service=$(ynh_app_setting_get --app=$app --key=fpm_dedicated_service)
dedicated_service=${dedicated_service:-0}
# Get the version of PHP used by this app
local phpversion=$(ynh_app_setting_get --app=$app --key=phpversion)
@ -218,7 +296,69 @@ ynh_remove_fpm_config() {
fi
ynh_secure_remove --file="$fpm_config_dir/pool.d/$app.conf"
if [ -e $fpm_config_dir/conf.d/20-$app.ini ]; then
ynh_secure_remove --file="$fpm_config_dir/conf.d/20-$app.ini"
fi
if [ $dedicated_service -eq 1 ]; then
# Remove the dedicated service PHP-FPM service for the app
ynh_remove_systemd_config --service=$fpm_service
# Remove the global PHP-FPM conf
ynh_secure_remove --file="$fpm_config_dir/php-fpm-$app.conf"
# Remove the service from the list of services known by YunoHost
yunohost service remove $fpm_service
elif ynh_package_is_installed --package="php${phpversion}-fpm"; then
ynh_systemd_action --service_name=$fpm_service --action=reload
fi
# If the PHP version used is not the default version for YunoHost
# The second part with YNH_APP_PURGE is an ugly hack to guess that we're inside the remove script
# (we don't actually care about its value, we just check its not empty hence it exists)
if [ "$phpversion" != "$YNH_DEFAULT_PHP_VERSION" ] && [ -n "${YNH_APP_PURGE:-}" ] && dpkg --compare-versions ${YNH_APP_PACKAGING_FORMAT:-0} lt 2; then
# Remove app dependencies ... but ideally should happen via an explicit call from packager
ynh_remove_app_dependencies
fi
}
# Install another version of PHP.
#
# [internal]
#
# Legacy, to be remove on bullseye
#
# usage: ynh_install_php --phpversion=phpversion [--package=packages]
# | arg: -v, --phpversion= - Version of PHP to install.
# | arg: -p, --package= - Additionnal PHP packages to install
#
# Requires YunoHost version 3.8.1 or higher.
ynh_install_php() {
# Declare an array to define the options of this helper.
local legacy_args=vp
local -A args_array=([v]=phpversion= [p]=package=)
local phpversion
local package
# Manage arguments with getopts
ynh_handle_getopts_args "$@"
package=${package:-}
if [ "$phpversion" == "$YNH_DEFAULT_PHP_VERSION" ]; then
ynh_die --message="Do not use ynh_install_php to install php$YNH_DEFAULT_PHP_VERSION"
fi
ynh_install_app_dependencies "$package"
}
# Remove the specific version of PHP used by the app.
#
# [internal]
#
# Legacy, to be remove on bullseye
#
# usage: ynh_remove_php
#
# Requires YunoHost version 3.8.1 or higher.
ynh_remove_php() {
ynh_remove_app_dependencies
}
# Define the values to configure PHP-FPM

View file

@ -1,7 +1,7 @@
#!/bin/bash
PSQL_ROOT_PWD_FILE=/etc/yunohost/psql
PSQL_VERSION=15
PSQL_VERSION=13
# Open a connection as a user
#

View file

@ -18,7 +18,11 @@ ynh_app_setting_get() {
ynh_handle_getopts_args "$@"
app="${app:-$_globalapp}"
if [[ $key =~ (unprotected|protected|skipped)_ ]]; then
yunohost app setting $app $key
else
ynh_app_setting "get" "$app" "$key"
fi
}
# Set an application setting
@ -41,7 +45,11 @@ ynh_app_setting_set() {
ynh_handle_getopts_args "$@"
app="${app:-$_globalapp}"
if [[ $key =~ (unprotected|protected|skipped)_ ]]; then
yunohost app setting $app $key -v $value
else
ynh_app_setting "set" "$app" "$key" "$value"
fi
}
# Set an application setting but only if the "$key" variable ain't set yet
@ -98,7 +106,11 @@ ynh_app_setting_delete() {
ynh_handle_getopts_args "$@"
app="${app:-$_globalapp}"
if [[ "$key" =~ (unprotected|skipped|protected)_ ]]; then
yunohost app setting $app $key -d
else
ynh_app_setting "delete" "$app" "$key"
fi
}
# Small "hard-coded" interface to avoid calling "yunohost app" directly each

View file

@ -367,12 +367,16 @@ ynh_compare_current_package_version() {
_ynh_apply_default_permissions() {
local target=$1
local ynh_requirement=$(ynh_read_manifest --manifest_key="requirements.yunohost" | tr -d '<>= ')
if dpkg --compare-versions ${YNH_APP_PACKAGING_FORMAT:-0} ge 2 || [ -z "$ynh_requirement" ] || [ "$ynh_requirement" == "null" ] || dpkg --compare-versions $ynh_requirement ge 4.2; then
chmod o-rwx $target
chmod g-w $target
chown -R root:root $target
if ynh_system_user_exists $app; then
chown $app:$app $target
fi
fi
# Crons should be owned by root
# Also we don't want systemd conf, nginx conf or others stuff to be owned by the app,
@ -384,7 +388,7 @@ _ynh_apply_default_permissions() {
}
int_to_bool() {
sed -e 's/^1$/True/g' -e 's/^0$/False/g' -e 's/^true$/True/g' -e 's/^false$/False/g'
sed -e 's/^1$/True/g' -e 's/^0$/False/g'
}
toml_to_json() {

13
hooks/backup/27-data_xmpp Normal file
View file

@ -0,0 +1,13 @@
#!/bin/bash
# Exit hook on subcommand error or unset variable
set -eu
# Source YNH helpers
source /usr/share/yunohost/helpers
# Backup destination
backup_dir="${1}/data/xmpp"
ynh_backup /var/lib/metronome "${backup_dir}/var_lib_metronome"
ynh_backup /var/xmpp-upload/ "${backup_dir}/var_xmpp-upload"

View file

@ -2,167 +2,15 @@
set -e
base_folder_and_perm_init() {
#############################
# Base yunohost conf folder #
#############################
mkdir -p /etc/yunohost
# NB: x permission for 'others' is important for ssl-cert (and maybe mdns), otherwise slapd will fail to start because can't access the certs
chmod 755 /etc/yunohost
################
# Logs folders #
################
mkdir -p /var/log/yunohost
chown root:root /var/log/yunohost
chmod 750 /var/log/yunohost
##################
# Portal folders #
##################
getent passwd ynh-portal &>/dev/null || useradd --no-create-home --shell /usr/sbin/nologin --system --user-group ynh-portal
mkdir -p /etc/yunohost/portal
chmod 500 /etc/yunohost/portal
chown ynh-portal:ynh-portal /etc/yunohost/portal
mkdir -p /usr/share/yunohost/portal/customassets
chmod 775 /usr/share/yunohost/portal/customassets
chown root:root /usr/share/yunohost/portal/customassets
touch /var/log/yunohost-portalapi.log
chown ynh-portal:root /var/log/yunohost-portalapi.log
chmod 600 /var/log/yunohost-portalapi.log
###############################
# Sessions folder and secrets #
###############################
# Portal
mkdir -p /var/cache/yunohost-portal/sessions
chown ynh-portal:www-data /var/cache/yunohost-portal
chmod 510 /var/cache/yunohost-portal
chown ynh-portal:www-data /var/cache/yunohost-portal/sessions
chmod 710 /var/cache/yunohost-portal/sessions
# Webadmin
mkdir -p /var/cache/yunohost/sessions
chown root:root /var/cache/yunohost/sessions
chmod 700 /var/cache/yunohost/sessions
if test -e /etc/yunohost/installed
then
# Initialize session secrets
# Obviously we only do this in the post_regen, ie during the postinstall, because we don't want every pre-installed instance to have the same secret
if [ ! -e /etc/yunohost/.admin_cookie_secret ]; then
dd if=/dev/urandom bs=1 count=1000 2>/dev/null | tr --complement --delete 'A-Za-z0-9' | head -c 64 > /etc/yunohost/.admin_cookie_secret
fi
chown root:root /etc/yunohost/.admin_cookie_secret
chmod 400 /etc/yunohost/.admin_cookie_secret
if [ ! -e /etc/yunohost/.ssowat_cookie_secret ]; then
# NB: we need this to be exactly 32 char long, because it is later used as a key for AES256
dd if=/dev/urandom bs=1 count=1000 2>/dev/null | tr --complement --delete 'A-Za-z0-9' | head -c 32 > /etc/yunohost/.ssowat_cookie_secret
fi
chown ynh-portal:root /etc/yunohost/.ssowat_cookie_secret
chmod 400 /etc/yunohost/.ssowat_cookie_secret
fi
##################
# Domain folders #
##################
mkdir -p /etc/yunohost/domains
chown root /etc/yunohost/domains
chmod 700 /etc/yunohost/domains
###############
# App folders #
###############
mkdir -p /etc/yunohost/apps
chown root /etc/yunohost/apps
chmod 700 /etc/yunohost/apps
#####################
# Apps data folders #
#####################
mkdir -p /home/yunohost.app
chmod 755 /home/yunohost.app
################
# Certs folder #
################
mkdir -p /etc/yunohost/certs
chown -R root:ssl-cert /etc/yunohost/certs
chmod 750 /etc/yunohost/certs
# We do this with find because there could be a lot of them...
find /etc/yunohost/certs/ -type f -exec chmod 640 {} \;
find /etc/yunohost/certs/ -type d -exec chmod 750 {} \;
##################
# Backup folders #
##################
mkdir -p /home/yunohost.backup/archives
chmod 770 /home/yunohost.backup
chmod 770 /home/yunohost.backup/archives
if test -e /etc/yunohost/installed
then
# The admins group only exist after the postinstall
chown root:admins /home/yunohost.backup
chown root:admins /home/yunohost.backup/archives
else
chown root:root /home/yunohost.backup
chown root:root /home/yunohost.backup/archives
fi
########
# Misc #
########
mkdir -p /etc/yunohost/hooks.d
chown root /etc/yunohost/hooks.d
chmod 700 /etc/yunohost/hooks.d
mkdir -p /var/cache/yunohost/repo
chown root:root /var/cache/yunohost
chmod 700 /var/cache/yunohost
[ ! -e /var/www/.well-known/ynh-diagnosis/ ] || chmod 775 /var/www/.well-known/ynh-diagnosis/
if test -e /etc/yunohost/installed
then
setfacl -m g:all_users:--- /var/www
setfacl -m g:all_users:--- /var/log/nginx
setfacl -m g:all_users:--- /etc/yunohost
setfacl -m g:all_users:--- /etc/ssowat
fi
}
do_init_regen() {
if [[ $EUID -ne 0 ]]; then
echo "You must be root to run this script" 1>&2
exit 1
fi
cd /usr/share/yunohost/conf/yunohost
base_folder_and_perm_init
# Empty ssowat json persistent conf
echo "{}" >'/etc/ssowat/conf.json.persistent'
chmod 644 /etc/ssowat/conf.json.persistent
chown root:root /etc/ssowat/conf.json.persistent
echo "{}" >'/etc/ssowat/conf.json'
chmod 644 /etc/ssowat/conf.json
chown root:root /etc/ssowat/conf.json
# Empty service conf
touch /etc/yunohost/services.yml
[[ -d /etc/yunohost ]] || mkdir -p /etc/yunohost
# set default current_host
[[ -f /etc/yunohost/current_host ]] \
@ -176,9 +24,39 @@ do_init_regen() {
[[ -d /etc/skel/media ]] \
|| (mkdir -p /media && ln -s /media /etc/skel/media)
# YunoHost services
# Cert folders
mkdir -p /etc/yunohost/certs
chown -R root:ssl-cert /etc/yunohost/certs
chmod 750 /etc/yunohost/certs
# App folders
mkdir -p /etc/yunohost/apps
chmod 700 /etc/yunohost/apps
mkdir -p /home/yunohost.app
chmod 755 /home/yunohost.app
# Domain settings
mkdir -p /etc/yunohost/domains
chmod 700 /etc/yunohost/domains
# Backup folders
mkdir -p /home/yunohost.backup/archives
chmod 750 /home/yunohost.backup/archives
chown root:root /home/yunohost.backup/archives # This is later changed to root:admins once the admins group exists
# Empty ssowat json persistent conf
echo "{}" > '/etc/ssowat/conf.json.persistent'
chmod 644 /etc/ssowat/conf.json.persistent
chown root:root /etc/ssowat/conf.json.persistent
# Empty service conf
touch /etc/yunohost/services.yml
mkdir -p /var/cache/yunohost/repo
chown root:root /var/cache/yunohost
chmod 700 /var/cache/yunohost
cp yunohost-api.service /etc/systemd/system/yunohost-api.service
cp yunohost-portal-api.service /etc/systemd/system/yunohost-portal-api.service
cp yunohost-firewall.service /etc/systemd/system/yunohost-firewall.service
cp yunoprompt.service /etc/systemd/system/yunoprompt.service
@ -187,9 +65,6 @@ do_init_regen() {
systemctl enable yunohost-api.service --quiet
systemctl start yunohost-api.service
systemctl enable yunohost-portal-api.service --quiet
systemctl start yunohost-portal-api.service
# Enable yunoprompt (in particular for installs from ISO where we want this to show on first boot instead of asking for a login/password)
systemctl enable yunoprompt --quiet
@ -282,7 +157,6 @@ HandleLidSwitchExternalPower=ignore
EOF
cp yunohost-api.service ${pending_dir}/etc/systemd/system/yunohost-api.service
cp yunohost-portal-api.service ${pending_dir}/etc/systemd/system/yunohost-portal-api.service
cp yunohost-firewall.service ${pending_dir}/etc/systemd/system/yunohost-firewall.service
cp yunoprompt.service ${pending_dir}/etc/systemd/system/yunoprompt.service
cp proc-hidepid.service ${pending_dir}/etc/systemd/system/proc-hidepid.service
@ -297,45 +171,62 @@ EOF
do_post_regen() {
regen_conf_files=$1
# Re-mkdir / apply permission to all basic folders etc
base_folder_and_perm_init
######################
# Enfore permissions #
######################
# Legacy log tree structure
if [ ! -e /var/log/yunohost/operations ]
then
mkdir -p /var/log/yunohost/operations
fi
if [ -d /var/log/yunohost/categories/operation ] && [ ! -L /var/log/yunohost/categories/operation ]
then
# (we use find -type f instead of mv /folder/* to make sure to also move hidden files which are not included in globs by default)
find /var/log/yunohost/categories/operation/ -type f -print0 | xargs -0 -I {} mv {} /var/log/yunohost/operations/
# Attempt to delete the old dir (because we want it to be a symlink) or just rename it if it can't be removed (not empty) for some reason
rmdir /var/log/yunohost/categories/operation || mv /var/log/yunohost/categories/operation /var/log/yunohost/categories/operation.old
ln -s /var/log/yunohost/operations /var/log/yunohost/categories/operation
fi
chmod 770 /home/yunohost.backup
chmod 770 /home/yunohost.backup/archives
chmod 700 /var/cache/yunohost
chown root:admins /home/yunohost.backup
chown root:admins /home/yunohost.backup/archives
chown root:root /var/cache/yunohost
[ ! -e /var/www/.well-known/ynh-diagnosis/ ] || chmod 775 /var/www/.well-known/ynh-diagnosis/
# NB: x permission for 'others' is important for ssl-cert (and maybe mdns), otherwise slapd will fail to start because can't access the certs
chmod 755 /etc/yunohost
# Make sure conf files why may be created by apps are owned and writable only by root
find /etc/systemd/system/*.service -type f | xargs -r chown root:root
find /etc/systemd/system/*.service -type f | xargs -r chmod 0644
if ls -l /etc/php/*/fpm/pool.d/*.conf 2>/dev/null; then
if ls -l /etc/php/*/fpm/pool.d/*.conf; then
chown root:root /etc/php/*/fpm/pool.d/*.conf
chmod 644 /etc/php/*/fpm/pool.d/*.conf
fi
# Certs
# We do this with find because there could be a lot of them...
chown -R root:ssl-cert /etc/yunohost/certs
chmod 750 /etc/yunohost/certs
find /etc/yunohost/certs/ -type f -exec chmod 640 {} \;
find /etc/yunohost/certs/ -type d -exec chmod 750 {} \;
find /etc/cron.*/yunohost-* -type f -exec chmod 755 {} \;
find /etc/cron.d/yunohost-* -type f -exec chmod 644 {} \;
find /etc/cron.*/yunohost-* -type f -exec chown root:root {} \;
setfacl -m g:all_users:--- /var/www
setfacl -m g:all_users:--- /var/log/nginx
setfacl -m g:all_users:--- /etc/yunohost
setfacl -m g:all_users:--- /etc/ssowat
for USER in $(yunohost user list --quiet --output-as json | jq -r '.users | .[] | .username'); do
[ ! -e "/home/$USER" ] || setfacl -m g:all_users:--- /home/$USER
done
# Domain settings
mkdir -p /etc/yunohost/domains
# Misc configuration / state files
chown root:root $(ls /etc/yunohost/{*.yml,*.yaml,*.json,mysql,psql} 2> /dev/null | grep -vw mdns.yml)
chmod 600 $(ls /etc/yunohost/{*.yml,*.yaml,*.json,mysql,psql} 2> /dev/null)
# Apps folder, custom hooks folder
[[ ! -e /etc/yunohost/hooks.d ]] || (chown root /etc/yunohost/hooks.d && chmod 700 /etc/yunohost/hooks.d)
[[ ! -e /etc/yunohost/apps ]] || (chown root /etc/yunohost/apps && chmod 700 /etc/yunohost/apps)
[[ ! -e /etc/yunohost/domains ]] || (chown root /etc/yunohost/domains && chmod 700 /etc/yunohost/domains)
# Create ssh.app and sftp.app groups if they don't exist yet
grep -q '^ssh.app:' /etc/group || groupadd ssh.app
grep -q '^sftp.app:' /etc/group || groupadd sftp.app
@ -347,7 +238,6 @@ do_post_regen() {
systemctl restart ntp
}
fi
[[ ! "$regen_conf_files" =~ "nftables.service.d/ynh-override.conf" ]] || systemctl daemon-reload
[[ ! "$regen_conf_files" =~ "login.conf.d/ynh-override.conf" ]] || {
systemctl daemon-reload
@ -355,7 +245,6 @@ do_post_regen() {
}
[[ ! "$regen_conf_files" =~ "yunohost-firewall.service" ]] || systemctl daemon-reload
[[ ! "$regen_conf_files" =~ "yunohost-api.service" ]] || systemctl daemon-reload
[[ ! "$regen_conf_files" =~ "yunohost-portal-api.service" ]] || systemctl daemon-reload
if [[ "$regen_conf_files" =~ "yunoprompt.service" ]]; then
systemctl daemon-reload
@ -368,9 +257,6 @@ do_post_regen() {
systemctl $action proc-hidepid --quiet --now
fi
systemctl enable yunohost-portal-api.service --quiet
systemctl is-active yunohost-portal-api --quiet || systemctl start yunohost-portal-api.service
# Change dpkg vendor
# see https://wiki.debian.org/Derivatives/Guidelines#Vendor
if readlink -f /etc/dpkg/origins/default | grep -q debian; then

View file

@ -9,16 +9,17 @@ do_pre_regen() {
cd /usr/share/yunohost/conf/ssh
# Support different strategy for security configurations
export compatibility="$(jq -r '.ssh_compatibility' <<< "$YNH_SETTINGS")"
export port="$(jq -r '.ssh_port' <<< "$YNH_SETTINGS")"
export password_authentication="$(jq -r '.ssh_password_authentication' <<< "$YNH_SETTINGS" | int_to_bool)"
export ssh_keys=$(ls /etc/ssh/ssh_host_{ed25519,rsa,ecdsa}_key 2>/dev/null || true)
# do not listen to IPv6 if unavailable
[[ -f /proc/net/if_inet6 ]] && ipv6_enabled=true || ipv6_enabled=false
export ipv6_enabled
ssh_keys=$(ls /etc/ssh/ssh_host_{ed25519,rsa,ecdsa}_key 2> /dev/null || true)
# Support different strategy for security configurations
export compatibility="$(yunohost settings get 'security.ssh.ssh_compatibility')"
export port="$(yunohost settings get 'security.ssh.ssh_port')"
export password_authentication="$(yunohost settings get 'security.ssh.ssh_password_authentication' | int_to_bool)"
export ssh_keys
export ipv6_enabled
ynh_render_template "sshd_config" "${pending_dir}/etc/ssh/sshd_config"
}

View file

@ -8,6 +8,10 @@ config="/usr/share/yunohost/conf/slapd/config.ldif"
db_init="/usr/share/yunohost/conf/slapd/db_init.ldif"
do_init_regen() {
if [[ $EUID -ne 0 ]]; then
echo "You must be root to run this script" 1>&2
exit 1
fi
do_pre_regen ""

View file

@ -2,7 +2,7 @@
set -e
readonly YNH_DEFAULT_PHP_VERSION=8.2
readonly YNH_DEFAULT_PHP_VERSION=7.4
do_pre_regen() {
pending_dir=$1
@ -11,7 +11,7 @@ do_pre_regen() {
# Add sury
mkdir -p ${pending_dir}/etc/apt/sources.list.d/
echo "deb [signed-by=/etc/apt/trusted.gpg.d/extra_php_version.gpg] https://packages.sury.org/php/ $(lsb_release --codename --short) main" > "${pending_dir}/etc/apt/sources.list.d/extra_php_version.list"
echo "deb https://packages.sury.org/php/ $(lsb_release --codename --short) main" > "${pending_dir}/etc/apt/sources.list.d/extra_php_version.list"
# Ban some packages from sury
echo "
@ -27,20 +27,6 @@ Pin: origin \"packages.sury.org\"
Pin-Priority: -1" >> "${pending_dir}/etc/apt/preferences.d/extra_php_version"
done
# Add yarn
echo "deb [signed-by=/etc/apt/trusted.gpg.d/yarn.gpg] https://dl.yarnpkg.com/debian/ stable main" > "${pending_dir}/etc/apt/sources.list.d/yarn.list"
# Ban everything from Yarn except Yarn
echo "
Package: *
Pin: origin \"dl.yarnpkg.com\"
Pin-Priority: -1
Package: yarn
Pin: origin \"dl.yarnpkg.com\"
Pin-Priority: 500" >>"${pending_dir}/etc/apt/preferences.d/yarn"
# Ban apache2, bind9
echo "
# PLEASE READ THIS WARNING AND DON'T EDIT THIS FILE
@ -85,12 +71,6 @@ do_post_regen() {
wget --timeout 900 --quiet "https://packages.sury.org/php/apt.gpg" --output-document=- | gpg --dearmor > "/etc/apt/trusted.gpg.d/extra_php_version.gpg"
fi
# Similar to Sury
if [[ ! -s /etc/apt/trusted.gpg.d/yarn.gpg ]]
then
wget --timeout 900 --quiet "https://dl.yarnpkg.com/debian/pubkey.gpg" --output-document=- | gpg --dearmor >"/etc/apt/trusted.gpg.d/yarn.gpg"
fi
# Make sure php7.4 is the default version when using php in cli
if test -e /usr/bin/php$YNH_DEFAULT_PHP_VERSION; then
update-alternatives --set php /usr/bin/php$YNH_DEFAULT_PHP_VERSION

91
hooks/conf_regen/12-metronome Executable file
View file

@ -0,0 +1,91 @@
#!/bin/bash
set -e
if ! dpkg --list | grep -q 'ii *metronome '; then
echo 'metronome is not installed, skipping'
exit 0
fi
do_pre_regen() {
pending_dir=$1
cd /usr/share/yunohost/conf/metronome
# create directories for pending conf
metronome_dir="${pending_dir}/etc/metronome"
metronome_conf_dir="${metronome_dir}/conf.d"
mkdir -p "$metronome_conf_dir"
# retrieve variables
main_domain=$(cat /etc/yunohost/current_host)
# install main conf file
cat metronome.cfg.lua \
| sed "s/{{ main_domain }}/${main_domain}/g" \
> "${metronome_dir}/metronome.cfg.lua"
# Trick such that old conf files are flagged as to remove
for domain in $YNH_DOMAINS; do
touch "${metronome_conf_dir}/${domain}.cfg.lua"
done
# add domain conf files
domain_list="$(yunohost domain list --features xmpp --output-as json | jq -r ".domains[]")"
for domain in $domain_list; do
cat domain.tpl.cfg.lua \
| sed "s/{{ domain }}/${domain}/g" \
> "${metronome_conf_dir}/${domain}.cfg.lua"
done
# remove old domain conf files
conf_files=$(ls -1 /etc/metronome/conf.d \
| awk '/^[^\.]+\.[^\.]+.*\.cfg\.lua$/ { print $1 }')
for file in $conf_files; do
domain=${file%.cfg.lua}
[[ $YNH_DOMAINS =~ $domain ]] \
|| touch "${metronome_conf_dir}/${file}"
done
}
do_post_regen() {
regen_conf_files=$1
# retrieve variables
main_domain=$(cat /etc/yunohost/current_host)
# create metronome directories for domains
for domain in $YNH_MAIN_DOMAINS; do
mkdir -p "/var/lib/metronome/${domain//./%2e}/pep"
# http_upload directory must be writable by metronome and readable by nginx
mkdir -p "/var/xmpp-upload/${domain}/upload"
# sgid bit allows that file created in that dir will be owned by www-data
# despite the fact that metronome ain't in the www-data group
chmod g+s "/var/xmpp-upload/${domain}/upload"
done
# fix some permissions
[ ! -e '/var/xmpp-upload' ] || chown -R metronome:www-data "/var/xmpp-upload/"
[ ! -e '/var/xmpp-upload' ] || chmod 750 "/var/xmpp-upload/"
# metronome should be in ssl-cert group to let it access SSL certificates
usermod -aG ssl-cert metronome
chown -R metronome: /var/lib/metronome/
chown -R metronome: /etc/metronome/conf.d/
if [[ -z "$(ls /etc/metronome/conf.d/*.cfg.lua 2> /dev/null)" ]]; then
if systemctl is-enabled metronome &> /dev/null; then
systemctl disable metronome --now 2> /dev/null
fi
else
if ! systemctl is-enabled metronome &> /dev/null; then
systemctl enable metronome --now 2> /dev/null
sleep 3
fi
[[ -z "$regen_conf_files" ]] \
|| systemctl restart metronome
fi
}
do_$1_regen ${@:2}

View file

@ -4,20 +4,25 @@ set -e
. /usr/share/yunohost/helpers
do_base_regen() {
do_init_regen() {
if [[ $EUID -ne 0 ]]; then
echo "You must be root to run this script" 1>&2
exit 1
fi
pending_dir=$1
nginx_dir="${pending_dir}/etc/nginx"
cd /usr/share/yunohost/conf/nginx
nginx_dir="/etc/nginx"
nginx_conf_dir="${nginx_dir}/conf.d"
mkdir -p "$nginx_conf_dir"
# install plain conf files
cp acme-challenge.conf.inc "$nginx_conf_dir"
cp global.conf "$nginx_conf_dir"
cp ssowat.conf "$nginx_conf_dir"
cp yunohost_http_errors.conf.inc "$nginx_conf_dir"
cp yunohost_sso.conf.inc "$nginx_conf_dir"
cp plain/* "$nginx_conf_dir"
# probably run with init: just disable default site, restart NGINX and exit
rm -f "${nginx_dir}/sites-enabled/default"
export compatibility="intermediate"
ynh_render_template "security.conf.inc" "${nginx_conf_dir}/security.conf.inc"
ynh_render_template "yunohost_admin.conf" "${nginx_conf_dir}/yunohost_admin.conf"
ynh_render_template "yunohost_admin.conf.inc" "${nginx_conf_dir}/yunohost_admin.conf.inc"
@ -25,17 +30,6 @@ do_base_regen() {
mkdir -p $nginx_conf_dir/default.d/
cp "redirect_to_admin.conf" $nginx_conf_dir/default.d/
}
do_init_regen() {
cd /usr/share/yunohost/conf/nginx
export compatibility="intermediate"
do_base_regen ""
# probably run with init: just disable default site, restart NGINX and exit
rm -f "${nginx_dir}/sites-enabled/default"
# Restart nginx if conf looks good, otherwise display error and exit unhappy
nginx -t 2> /dev/null || {
@ -59,21 +53,27 @@ do_pre_regen() {
nginx_conf_dir="${nginx_dir}/conf.d"
mkdir -p "$nginx_conf_dir"
export webadmin_allowlist_enabled="$(jq -r '.webadmin_allowlist_enabled' <<< "$YNH_SETTINGS" | int_to_bool)"
if [ "$webadmin_allowlist_enabled" == "True" ]; then
export webadmin_allowlist="$(jq -r '.webadmin_allowlist' <<< "$YNH_SETTINGS" | sed 's/^null$//g')"
# install / update plain conf files
cp plain/* "$nginx_conf_dir"
# remove the panel overlay if this is specified in settings
panel_overlay=$(yunohost settings get 'misc.portal.ssowat_panel_overlay_enabled' | int_to_bool)
if [ "$panel_overlay" == "False" ]; then
echo "#" > "${nginx_conf_dir}/yunohost_panel.conf.inc"
fi
# Support different strategy for security configurations
export redirect_to_https="$(jq -r '.nginx_redirect_to_https' <<< "$YNH_SETTINGS" | int_to_bool)"
export compatibility="$(jq -r '.nginx_compatibility' <<< "$YNH_SETTINGS" | int_to_bool)"
export experimental="$(jq -r '.security_experimental_enabled' <<< "$YNH_SETTINGS" | int_to_bool)"
# retrieve variables
main_domain=$(cat /etc/yunohost/current_host)
do_base_regen "${pending_dir}"
# Support different strategy for security configurations
export redirect_to_https="$(yunohost settings get 'security.nginx.nginx_redirect_to_https' | int_to_bool)"
export compatibility="$(yunohost settings get 'security.nginx.nginx_compatibility')"
export experimental="$(yunohost settings get 'security.experimental.security_experimental_enabled' | int_to_bool)"
ynh_render_template "security.conf.inc" "${nginx_conf_dir}/security.conf.inc"
cert_status=$(yunohost domain cert status --json)
# add domain conf files
xmpp_domain_list="$(yunohost domain list --features xmpp --output-as json | jq -r ".domains[]")"
mail_domain_list="$(yunohost domain list --features mail_in mail_out --output-as json | jq -r ".domains[]")"
for domain in $YNH_DOMAINS; do
domain_conf_dir="${nginx_conf_dir}/${domain}.d"
@ -86,6 +86,11 @@ do_pre_regen() {
export domain_cert_ca=$(echo $cert_status \
| jq ".certificates.\"$domain\".CA_type" \
| tr -d '"')
if echo "$xmpp_domain_list" | grep -q "^$domain$"; then
export xmpp_enabled="True"
else
export xmpp_enabled="False"
fi
if echo "$mail_domain_list" | grep -q "^$domain$"; then
export mail_enabled="True"
else
@ -101,8 +106,15 @@ do_pre_regen() {
done
# Legacy file to remove, but we can't really remove it because it may be included by app confs...
echo "# The old yunohost panel/tile/button doesn't exists anymore" > "$nginx_conf_dir"/yunohost_panel.conf.inc
export webadmin_allowlist_enabled=$(yunohost settings get security.webadmin.webadmin_allowlist_enabled | int_to_bool)
if [ "$webadmin_allowlist_enabled" == "True" ]; then
export webadmin_allowlist=$(yunohost settings get security.webadmin.webadmin_allowlist)
fi
ynh_render_template "yunohost_admin.conf.inc" "${nginx_conf_dir}/yunohost_admin.conf.inc"
ynh_render_template "yunohost_api.conf.inc" "${nginx_conf_dir}/yunohost_api.conf.inc"
ynh_render_template "yunohost_admin.conf" "${nginx_conf_dir}/yunohost_admin.conf"
mkdir -p $nginx_conf_dir/default.d/
cp "redirect_to_admin.conf" $nginx_conf_dir/default.d/
# remove old domain conf files
conf_files=$(ls -1 /etc/nginx/conf.d \

View file

@ -22,19 +22,19 @@ do_pre_regen() {
main_domain=$(cat /etc/yunohost/current_host)
# Support different strategy for security configurations
export compatibility="$(jq -r '.postfix_compatibility' <<< "$YNH_SETTINGS")"
export compatibility="$(yunohost settings get 'security.postfix.postfix_compatibility')"
# Add possibility to specify a relay
# Could be useful with some isp with no 25 port open or more complex setup
export relay_port=""
export relay_user=""
export relay_host=""
export relay_enabled="$(jq -r '.smtp_relay_enabled' <<< "$YNH_SETTINGS" | int_to_bool)"
export relay_enabled="$(yunohost settings get 'email.smtp.smtp_relay_enabled' | int_to_bool)"
if [ "${relay_enabled}" == "True" ]; then
relay_host="$(jq -r '.smtp_relay_host' <<< "$YNH_SETTINGS")"
relay_port="$(jq -r '.smtp_relay_port' <<< "$YNH_SETTINGS")"
relay_user="$(jq -r '.smtp_relay_user' <<< "$YNH_SETTINGS")"
relay_password="$(jq -r '.smtp_relay_password' <<< "$YNH_SETTINGS")"
relay_host="$(yunohost settings get 'email.smtp.smtp_relay_host')"
relay_port="$(yunohost settings get 'email.smtp.smtp_relay_port')"
relay_user="$(yunohost settings get 'email.smtp.smtp_relay_user')"
relay_password="$(yunohost settings get 'email.smtp.smtp_relay_password')"
# Avoid to display "Relay account paswword" to other users
touch ${postfix_dir}/sasl_passwd
@ -69,7 +69,7 @@ do_pre_regen() {
> "${default_dir}/postsrsd"
# adapt it for IPv4-only hosts
ipv6="$(jq -r '.smtp_allow_ipv6' <<< "$YNH_SETTINGS" | int_to_bool)"
ipv6="$(yunohost settings get 'email.smtp.smtp_allow_ipv6' | int_to_bool)"
if [ "$ipv6" == "False" ] || [ ! -f /proc/net/if_inet6 ]; then
sed -i \
's/ \[::ffff:127.0.0.0\]\/104 \[::1\]\/128//g' \

View file

@ -16,7 +16,7 @@ do_pre_regen() {
cp dovecot-ldap.conf "${dovecot_dir}/dovecot-ldap.conf"
cp dovecot.sieve "${dovecot_dir}/global_script/dovecot.sieve"
export pop3_enabled="$(jq -r '.pop3_enabled' <<< "$YNH_SETTINGS" | int_to_bool)"
export pop3_enabled="$(yunohost settings get 'email.pop3.pop3_enabled' | int_to_bool)"
export main_domain=$(cat /etc/yunohost/current_host)
export domain_list="$(yunohost domain list --features mail_in mail_out --output-as json | jq -r ".domains[]" | tr '\n' ' ')"
@ -42,7 +42,7 @@ do_post_regen() {
# create vmail user
id vmail > /dev/null 2>&1 \
|| { mkdir -p /var/vmail; adduser --system --ingroup mail --uid 500 vmail --home /var/vmail --no-create-home; }
|| adduser --system --ingroup mail --uid 500 vmail --home /var/vmail --no-create-home
# Delete legacy home for vmail that existed in the past but was empty, poluting /home/
[ ! -e /home/vmail ] || rmdir --ignore-fail-on-non-empty /home/vmail

View file

@ -1,43 +0,0 @@
#!/bin/bash
set -e
do_pre_regen() {
pending_dir=$1
cd /usr/share/yunohost/conf/opendkim
install -D -m 644 opendkim.conf "${pending_dir}/etc/opendkim.conf"
}
do_post_regen() {
mkdir -p /etc/dkim
# Create / empty those files because we're force-regenerating them
echo "" > /etc/dkim/keytable
echo "" > /etc/dkim/signingtable
# create DKIM key for domains
domain_list="$(yunohost domain list --features mail_in mail_out --output-as json | jq -r ".domains[]" | tr '\n' ' ')"
for domain in $domain_list; do
domain_key="/etc/dkim/${domain}.mail.key"
[ ! -f "$domain_key" ] && {
# We use a 1024 bit size because nsupdate doesn't seem to be able to
# handle 2048...
opendkim-genkey --domain="$domain" \
--selector=mail --directory=/etc/dkim -b 1024
mv /etc/dkim/mail.private "$domain_key"
mv /etc/dkim/mail.txt "/etc/dkim/${domain}.mail.txt"
}
echo "mail._domainkey.${domain} ${domain}:mail:${domain_key}" >> /etc/dkim/keytable
echo "*@$domain mail._domainkey.${domain}" >> /etc/dkim/signingtable
done
chown -R opendkim /etc/dkim/
chmod 700 /etc/dkim/
systemctl restart opendkim
}
do_$1_regen ${@:2}

65
hooks/conf_regen/31-rspamd Executable file
View file

@ -0,0 +1,65 @@
#!/bin/bash
set -e
do_pre_regen() {
pending_dir=$1
cd /usr/share/yunohost/conf/rspamd
install -D -m 644 metrics.local.conf \
"${pending_dir}/etc/rspamd/local.d/metrics.conf"
install -D -m 644 dkim_signing.conf \
"${pending_dir}/etc/rspamd/local.d/dkim_signing.conf"
install -D -m 644 rspamd.sieve \
"${pending_dir}/etc/dovecot/global_script/rspamd.sieve"
install -D -m 644 redis.conf \
"${pending_dir}/etc/rspamd/local.d/redis.conf"
}
do_post_regen() {
##
## DKIM key generation
##
# create DKIM directory with proper permission
mkdir -p /etc/dkim
chown _rspamd /etc/dkim
# create DKIM key for domains
domain_list="$(yunohost domain list --features mail_in mail_out --output-as json | jq -r ".domains[]" | tr '\n' ' ')"
for domain in $domain_list; do
domain_key="/etc/dkim/${domain}.mail.key"
[ ! -f "$domain_key" ] && {
# We use a 1024 bit size because nsupdate doesn't seem to be able to
# handle 2048...
opendkim-genkey --domain="$domain" \
--selector=mail --directory=/etc/dkim -b 1024
mv /etc/dkim/mail.private "$domain_key"
mv /etc/dkim/mail.txt "/etc/dkim/${domain}.mail.txt"
}
done
# fix DKIM keys permissions
chown _rspamd /etc/dkim/*.mail.key
chmod 400 /etc/dkim/*.mail.key
[ ! -e /var/log/rspamd ] || chown -R _rspamd:_rspamd /var/log/rspamd
regen_conf_files=$1
[ -z "$regen_conf_files" ] && exit 0
# compile sieve script
[[ "$regen_conf_files" =~ rspamd\.sieve ]] && {
sievec /etc/dovecot/global_script/rspamd.sieve
chown -R vmail:mail /etc/dovecot/global_script
systemctl restart dovecot
}
# Restart rspamd due to the upgrade
# https://rspamd.com/announce/2016/08/01/rspamd-1.3.1.html
systemctl -q restart rspamd.service
}
do_$1_regen ${@:2}

13
hooks/conf_regen/36-redis Executable file
View file

@ -0,0 +1,13 @@
#!/bin/bash
do_pre_regen() {
:
}
do_post_regen() {
# Enforce these damn permissions because for some reason in some weird cases
# they are spontaneously replaced by root:root -_-
chown -R redis:adm /var/log/redis
}
do_$1_regen ${@:2}

View file

@ -14,11 +14,10 @@ do_pre_regen() {
mkdir -p "${fail2ban_dir}/jail.d"
cp yunohost.conf "${fail2ban_dir}/filter.d/yunohost.conf"
cp yunohost-portal.conf "${fail2ban_dir}/filter.d/yunohost-portal.conf"
cp postfix-sasl.conf "${fail2ban_dir}/filter.d/postfix-sasl.conf"
cp jail.conf "${fail2ban_dir}/jail.conf"
export ssh_port="$(jq -r '.ssh_port' <<< "$YNH_SETTINGS")"
export ssh_port="$(yunohost settings get 'security.ssh.ssh_port')"
ynh_render_template "yunohost-jails.conf" "${fail2ban_dir}/jail.d/yunohost-jails.conf"
}

View file

@ -0,0 +1,4 @@
backup_dir="$1/data/xmpp"
cp -a $backup_dir/var_lib_metronome/. /var/lib/metronome
cp -a $backup_dir/var_xmpp-upload/. /var/xmpp-upload

View file

@ -16,6 +16,8 @@
"app_arch_not_supported": "This app can only be installed on architectures {required} but your server architecture is {current}",
"app_argument_choice_invalid": "Pick a valid value for argument '{name}': '{value}' is not among the available choices ({choices})",
"app_argument_invalid": "Pick a valid value for the argument '{name}': {error}",
"app_argument_password_no_default": "Error while parsing password argument '{name}': password argument can't have a default value for security reasons",
"app_argument_required": "Argument '{name}' is required",
"app_change_url_failed": "Could not change the url for {app}: {error}",
"app_change_url_identical_domains": "The old and new domain/url_path are identical ('{domain}{path}'), nothing to do.",
"app_change_url_no_script": "The app '{app_name}' doesn't support URL modification yet. Maybe you should upgrade it.",
@ -74,6 +76,7 @@
"app_yunohost_version_not_supported": "This app requires YunoHost >= {required} but current installed version is {current}",
"apps_already_up_to_date": "All apps are already up-to-date",
"apps_catalog_failed_to_download": "Unable to download the {apps_catalog} app catalog: {error}",
"apps_catalog_init_success": "App catalog system initialized!",
"apps_catalog_obsolete_cache": "The app catalog cache is empty or obsolete.",
"apps_catalog_update_success": "The application catalog has been updated!",
"apps_catalog_updating": "Updating application catalog…",
@ -91,7 +94,7 @@
"ask_new_domain": "New domain",
"ask_new_path": "New path",
"ask_password": "Password",
"ask_user_domain": "Domain to use for the user's email address",
"ask_user_domain": "Domain to use for the user's email address and XMPP account",
"backup_abstract_method": "This backup method has yet to be implemented",
"backup_actually_backuping": "Creating a backup archive from the collected files…",
"backup_app_failed": "Could not back up {app}",
@ -159,6 +162,7 @@
"certmanager_no_cert_file": "Could not read the certificate file for the domain {domain} (file: {file})",
"certmanager_self_ca_conf_file_not_found": "Could not find configuration file for self-signing authority (file: {file})",
"certmanager_unable_to_parse_self_CA_name": "Could not parse name of self-signing authority (file: {file})",
"certmanager_warning_subdomain_dns_record": "Subdomain '{subdomain}' does not resolve to the same IP address as '{domain}'. Some features will not be available until you fix this and regenerate the certificate.",
"config_action_disabled": "Could not run action '{action}' since it is disabled, make sure to meet its constraints. help: {help}",
"config_action_failed": "Failed to run action '{action}': {error}",
"config_apply_failed": "Applying the new configuration failed: {error}",
@ -167,6 +171,11 @@
"config_forbidden_readonly_type": "The type '{type}' can't be set as readonly, use another type to render this value (relevant arg id: '{id}').",
"config_no_panel": "No config panel found.",
"config_unknown_filter_key": "The filter key '{filter_key}' is incorrect.",
"config_validate_color": "Should be a valid RGB hexadecimal color",
"config_validate_date": "Should be a valid date like in the format YYYY-MM-DD",
"config_validate_email": "Should be a valid email",
"config_validate_time": "Should be a valid time like HH:MM",
"config_validate_url": "Should be a valid web URL",
"confirm_app_install_danger": "DANGER! This app is known to be still experimental (if not explicitly not working)! You should probably NOT install it unless you know what you are doing. NO SUPPORT will be provided if this app doesn't work or breaks your system… If you are willing to take that risk anyway, type '{answers}'",
"confirm_app_install_thirdparty": "DANGER! This app is not part of YunoHost's app catalog. Installing third-party apps may compromise the integrity and security of your system. You should probably NOT install it unless you know what you are doing. NO SUPPORT will be provided if this app doesn't work or breaks your system… If you are willing to take that risk anyway, type '{answers}'",
"confirm_app_install_warning": "Warning: This app may work, but is not well-integrated into YunoHost. Some features such as single sign-on and backup/restore might not be available. Install anyway? [{answers}] ",
@ -325,12 +334,14 @@
"diagnosis_swap_ok": "The system has {total} of swap!",
"diagnosis_swap_tip": "Please be careful and aware that if the server is hosting swap on an SD card or SSD storage, it may drastically reduce the life expectancy of the device.",
"diagnosis_unknown_categories": "The following categories are unknown: {categories}",
"diagnosis_using_stable_codename": "<cmd>apt</cmd> (the system's package manager) is currently configured to install packages from codename 'stable', instead of the codename of the current Debian version (bookworm).",
"diagnosis_using_stable_codename_details": "This is usually caused by incorrect configuration from your hosting provider. This is dangerous, because as soon as the next Debian version becomes the new 'stable', <cmd>apt</cmd> will want to upgrade all system packages without going through a proper migration procedure. It is recommended to fix this by editing the apt source for base Debian repository, and replace the <cmd>stable</cmd> keyword by <cmd>bookworm</cmd>. The corresponding configuration file should be <cmd>/etc/apt/sources.list</cmd>, or a file in <cmd>/etc/apt/sources.list.d/</cmd>.",
"diagnosis_using_stable_codename": "<cmd>apt</cmd> (the system's package manager) is currently configured to install packages from codename 'stable', instead of the codename of the current Debian version (bullseye).",
"diagnosis_using_stable_codename_details": "This is usually caused by incorrect configuration from your hosting provider. This is dangerous, because as soon as the next Debian version becomes the new 'stable', <cmd>apt</cmd> will want to upgrade all system packages without going through a proper migration procedure. It is recommended to fix this by editing the apt source for base Debian repository, and replace the <cmd>stable</cmd> keyword by <cmd>bullseye</cmd>. The corresponding configuration file should be <cmd>/etc/apt/sources.list</cmd>, or a file in <cmd>/etc/apt/sources.list.d/</cmd>.",
"diagnosis_using_yunohost_testing": "<cmd>apt</cmd> (the system's package manager) is currently configured to install any 'testing' upgrade for YunoHost core.",
"diagnosis_using_yunohost_testing_details": "This is probably OK if you know what you are doing, but pay attention to the release notes before installing YunoHost upgrades! If you want to disable 'testing' upgrades, you should remove the <cmd>testing</cmd> keyword from <cmd>/etc/apt/sources.list.d/yunohost.list</cmd>.",
"disk_space_not_sufficient_install": "There is not enough disk space left to install this application",
"disk_space_not_sufficient_update": "There is not enough disk space left to update this application",
"domain_cannot_add_muc_upload": "You cannot add domains starting with 'muc.'. This kind of name is reserved for the XMPP multi-users chat feature integrated into YunoHost.",
"domain_cannot_add_xmpp_upload": "You cannot add domains starting with 'xmpp-upload.'. This kind of name is reserved for the XMPP upload feature integrated into YunoHost.",
"domain_cannot_remove_main": "You cannot remove '{domain}' since it's the main domain, you first need to set another domain as the main domain using 'yunohost domain main-domain -n <another-domain>'; here is the list of candidate domains: {other_domains}",
"domain_cannot_remove_main_add_new_one": "You cannot remove '{domain}' since it's the main domain and your only domain, you need to first add another domain using 'yunohost domain add <another-domain.com>', then set is as the main domain using 'yunohost domain main-domain -n <another-domain.com>' and then you can remove the domain '{domain}' using 'yunohost domain remove {domain}'.",
"domain_cert_gen_failed": "Could not generate certificate",
@ -360,27 +371,8 @@
"domain_config_default_app_help": "People will automatically be redirected to this app when opening this domain. If no app is specified, people are redirected to the user portal login form.",
"domain_config_mail_in": "Incoming emails",
"domain_config_mail_out": "Outgoing emails",
"domain_config_enable_public_apps_page": "Show the list of public apps to visitors",
"domain_config_enable_public_apps_page_help": "Visitors will see a 'public apps' page when ending up on the portal instead of just the login form.",
"domain_config_custom_css": "Custom CSS stylesheet",
"domain_config_custom_css_help": "This is for advanced admins willing to customize the appearance of the portal",
"domain_config_portal_logo": "Custom logo",
"domain_config_portal_logo_help": "Accept .svg, .png and .jpeg. Prefer a monochrome .svg with <code>fill: currentColor</code> so that the logo adapts to the themes.",
"domain_config_feature_name": "Features",
"domain_config_portal_name": "Portal customization",
"domain_config_dns_name": "DNS",
"domain_config_cert_name": "Certificate",
"domain_config_portal_public_intro": "Custom public intro",
"domain_config_portal_public_intro_help": "You can use HTML, basic styles will be applied to generic elements.",
"domain_config_portal_theme": "Default theme",
"domain_config_portal_theme_help": "Users are allowed to choose another one in their settings.",
"domain_config_portal_title": "Custom title",
"domain_config_portal_user_intro": "Custom user intro",
"domain_config_portal_user_intro_help": "You can use HTML, basic styles will be applied to generic elements.",
"domain_config_search_engine": "Search engine URL",
"domain_config_search_engine_help": "This is an optional feature, allowing to display a search bar in the portal (for example if you like to use your YunoHost portal as your browser's home page). This should be an URL with an empty query string such as `https://duckduckgo.com/?q=`, with `q=` as duckduckgo's empty query parameter",
"domain_config_search_engine_name": "Search engine name",
"domain_config_show_other_domains_apps": "Show other domain's apps",
"domain_config_xmpp": "Instant messaging (XMPP)",
"domain_config_xmpp_help": "NB: some XMPP features will require that you update your DNS records and regenerate your Lets Encrypt certificate to be enabled",
"domain_created": "Domain created",
"domain_creation_failed": "Unable to create domain {domain}: {error}",
"domain_deleted": "Domain deleted",
@ -454,7 +446,9 @@
"global_settings_setting_nginx_redirect_to_https_help": "Redirect HTTP requests to HTTPs by default (DO NOT TURN OFF unless you really know what you're doing!)",
"global_settings_setting_passwordless_sudo": "Allow admins to use 'sudo' without re-typing their passwords",
"global_settings_setting_pop3_enabled": "Enable POP3",
"global_settings_setting_pop3_enabled_help": "Enable the POP3 protocol for the mail server. POP3 is an older protocol to access mailboxes from email clients and is more lightweight, but has less features than IMAP (enabled by default)",
"global_settings_setting_pop3_enabled_help": "Enable the POP3 protocol for the mail server",
"global_settings_setting_portal_theme": "Portal theme",
"global_settings_setting_portal_theme_help": "More info regarding creating custom portal themes at https://yunohost.org/theming",
"global_settings_setting_postfix_compatibility": "Postfix Compatibility",
"global_settings_setting_postfix_compatibility_help": "Compatibility vs. security tradeoff for the Postfix server. Affects the ciphers (and other security-related aspects)",
"global_settings_setting_root_access_explain": "On Linux systems, 'root' is the absolute admin. In YunoHost context, direct 'root' SSH login is by default disable - except from the local network of the server. Members of the 'admins' group can use the sudo command to act as root from the command line. However, it can be helpful to have a (robust) root password to debug the system if for some reason regular admins can not login anymore.",
@ -480,26 +474,13 @@
"global_settings_setting_ssh_password_authentication_help": "Allow password authentication for SSH",
"global_settings_setting_ssh_port": "SSH port",
"global_settings_setting_ssh_port_help": "A port lower than 1024 is preferred to prevent usurpation attempts by non-administrator services on the remote machine. You should also avoid using a port already in use, such as 80 or 443.",
"global_settings_setting_ssowat_panel_overlay_enabled": "Enable the small 'YunoHost' portal shortcut square on apps",
"global_settings_setting_user_strength": "User password strength requirements",
"global_settings_setting_user_strength_help": "These requirements are only enforced when initializing or changing the password",
"global_settings_setting_webadmin_allowlist": "Webadmin IP allowlist",
"global_settings_setting_webadmin_allowlist_enabled": "Enable Webadmin IP allowlist",
"global_settings_setting_webadmin_allowlist_enabled_help": "Allow only some IPs to access the webadmin.",
"global_settings_setting_webadmin_allowlist_help": "IP adresses allowed to access the webadmin. CIDR notation is allowed.",
"global_settings_setting_security_name": "Security",
"global_settings_setting_password_name": "Passwords",
"global_settings_setting_ssh_name": "SSH",
"global_settings_setting_nginx_name": "NGINX (web server)",
"global_settings_setting_postfix_name": "Postfix (SMTP email server)",
"global_settings_setting_webadmin_name": "Webadmin",
"global_settings_setting_root_access_name": "Change root password",
"global_settings_setting_experimental_name": "Experimental",
"global_settings_setting_email_name": "Email",
"global_settings_setting_pop3_name": "POP3",
"global_settings_setting_smtp_name": "SMTP",
"global_settings_setting_misc_name": "Other",
"global_settings_setting_backup_name": "Backup",
"global_settings_setting_network_name": "Network",
"good_practices_about_admin_password": "You are now about to define a new administration password. The password should be at least 8 characters long—though it is good practice to use a longer password (i.e. a passphrase) and/or to use a variation 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 long—though it is good practice to use a longer password (i.e. a passphrase) and/or to a variation of characters (uppercase, lowercase, digits and special characters).",
"group_already_exist": "Group {group} already exists",
@ -532,7 +513,8 @@
"installation_complete": "Installation completed",
"invalid_credentials": "Invalid password or username",
"invalid_number": "Must be a number",
"invalid_password": "Invalid password",
"invalid_number_max": "Must be lesser than {max}",
"invalid_number_min": "Must be greater than {min}",
"invalid_regex": "Invalid regex:'{regex}'",
"invalid_shell": "Invalid shell: {shell}",
"ip6tables_unavailable": "You cannot play with ip6tables here. You are either in a container or your kernel does not support it",
@ -593,8 +575,6 @@
"log_user_permission_update": "Update accesses for permission '{}'",
"log_user_update": "Update info for user '{}'",
"mail_alias_remove_failed": "Could not remove e-mail alias '{mail}'",
"mail_alias_unauthorized": "You are not authorized to add aliases related to domain '{domain}'",
"mail_already_exists": "Mail adress '{mail}' already exists",
"mail_domain_unknown": "Invalid e-mail address for domain '{domain}'. Please, use a domain administrated by this server.",
"mail_forward_remove_failed": "Could not remove e-mail forwarding '{mail}'",
"mail_unavailable": "This e-mail address is reserved for the admins group",
@ -602,6 +582,28 @@
"mailbox_used_space_dovecot_down": "The Dovecot mailbox service needs to be up if you want to fetch used mailbox space",
"main_domain_change_failed": "Unable to change the main domain",
"main_domain_changed": "The main domain has been changed",
"migration_0021_cleaning_up": "Cleaning up cache and packages not useful anymore…",
"migration_0021_general_warning": "Please note that this migration is a delicate operation. The YunoHost team did its best to review and test it, but the migration might still break parts of the system or its apps.\n\nTherefore, it is recommended to:\n - Perform a backup of any critical data or app. More info on https://yunohost.org/backup;\n - Be patient after launching the migration: Depending on your Internet connection and hardware, it might take up to a few hours for everything to upgrade.",
"migration_0021_main_upgrade": "Starting main upgrade…",
"migration_0021_modified_files": "Please note that the following files were found to be manually modified and might be overwritten following the upgrade: {manually_modified_files}",
"migration_0021_not_buster2": "The current Debian distribution is not Buster! If you already ran the Buster -> Bullseye migration, then this error is symptomatic of the fact that the migration procedure was not 100% succesful (otherwise YunoHost would have flagged it as completed). It is recommended to investigate what happened with the support team, who will need the **full** log of the migration, which can be found in Tools > Logs in the webadmin.",
"migration_0021_not_enough_free_space": "Free space is pretty low in /var/! You should have at least 1GB free to run this migration.",
"migration_0021_patch_yunohost_conflicts": "Applying patch to workaround conflict issue…",
"migration_0021_patching_sources_list": "Patching the sources.lists…",
"migration_0021_problematic_apps_warning": "Please note that the following possibly problematic installed apps were detected. It looks like those were not installed from the YunoHost app catalog, or are not flagged as 'working'. Consequently, it cannot be guaranteed that they will still work after the upgrade: {problematic_apps}",
"migration_0021_start": "Starting migration to Bullseye",
"migration_0021_still_on_buster_after_main_upgrade": "Something went wrong during the main upgrade, the system appears to still be on Debian Buster",
"migration_0021_system_not_fully_up_to_date": "Your system is not fully up-to-date. Please perform a regular upgrade before running the migration to Bullseye.",
"migration_0021_yunohost_upgrade": "Starting YunoHost core upgrade…",
"migration_0023_not_enough_space": "Make sufficient space available in {path} to run the migration.",
"migration_0023_postgresql_11_not_installed": "PostgreSQL was not installed on your system. Nothing to do.",
"migration_0023_postgresql_13_not_installed": "PostgreSQL 11 is installed, but not PostgreSQL 13!? Something weird might have happened on your system :(…",
"migration_0024_rebuild_python_venv_broken_app": "Skipping {app} because virtualenv can't easily be rebuilt for this app. Instead, you should fix the situation by forcing the upgrade of this app using `yunohost app upgrade --force {app}`.",
"migration_0024_rebuild_python_venv_disclaimer_base": "Following the upgrade to Debian Bullseye, some Python applications needs to be partially rebuilt to get converted to the new Python version shipped in Debian (in technical terms: what's called the 'virtualenv' needs to be recreated). In the meantime, those Python applications may not work. YunoHost can attempt to rebuild the virtualenv for some of those, as detailed below. For other apps, or if the rebuild attempt fails, you will need to manually force an upgrade for those apps.",
"migration_0024_rebuild_python_venv_disclaimer_ignored": "Virtualenvs can't be rebuilt automatically for those apps. You need to force an upgrade for those, which can be done from the command line with: `yunohost app upgrade --force APP`: {ignored_apps}",
"migration_0024_rebuild_python_venv_disclaimer_rebuild": "Rebuilding the virtualenv will be attempted for the following apps (NB: the operation may take some time!): {rebuild_apps}",
"migration_0024_rebuild_python_venv_failed": "Failed to rebuild the Python virtualenv for {app}. The app may not work as long as this is not resolved. You should fix the situation by forcing the upgrade of this app using `yunohost app upgrade --force {app}`.",
"migration_0024_rebuild_python_venv_in_progress": "Now attempting to rebuild the Python virtualenv for `{app}`",
"migration_0027_cleaning_up": "Cleaning up cache and packages not useful anymore…",
"migration_0027_delayed_api_restart": "The YunoHost API will automatically be restarted in 15 seconds. It may be unavailable for a few seconds, and then you will have to login again.",
"migration_0027_general_warning": "Please note that this migration is a delicate operation. The YunoHost team did its best to review and test it, but the migration might still break parts of the system or its apps.\n\nTherefore, it is recommended to:\n - Perform a backup of any critical data or app. More info on https://yunohost.org/backup;\n - Be patient after launching the migration: Depending on your Internet connection and hardware, it might take up to a few hours for everything to upgrade properly.",
@ -616,19 +618,13 @@
"migration_0027_still_on_bullseye_after_main_upgrade": "Something went wrong during the main upgrade, the system appears to still be on Debian Bullseye.",
"migration_0027_system_not_fully_up_to_date": "Your system is not fully up-to-date. Please perform a regular upgrade before running the migration to Bookworm.",
"migration_0027_yunohost_upgrade": "Starting YunoHost core upgrade…",
"migration_0029_not_enough_space": "Make sufficient space available in {path} to run the migration.",
"migration_0029_postgresql_13_not_installed": "PostgreSQL was not installed on your system. Nothing to do.",
"migration_0029_postgresql_15_not_installed": "PostgreSQL 13 is installed, but not PostgreSQL 15!? Something weird might have happened on your system :(…",
"migration_0030_rebuild_python_venv_in_bookworm_broken_app": "Skipping {app} because virtualenv can't easily be rebuilt for this app. Instead, you should fix the situation by forcing the upgrade of this app using `yunohost app upgrade --force {app}`.",
"migration_0030_rebuild_python_venv_in_bookworm_disclaimer_base": "Following the upgrade to Debian Bookworm, some Python applications needs to be partially rebuilt to get converted to the new Python version shipped in Debian (in technical terms: what's called the 'virtualenv' needs to be recreated). In the meantime, those Python applications may not work. YunoHost can attempt to rebuild the virtualenv for some of those, as detailed below. For other apps, or if the rebuild attempt fails, you will need to manually force an upgrade for those apps.",
"migration_0030_rebuild_python_venv_in_bookworm_disclaimer_ignored": "Virtualenvs can't be rebuilt automatically for those apps. You need to force an upgrade for those, which can be done from the command line with: `yunohost app upgrade --force APP`: {ignored_apps}",
"migration_0030_rebuild_python_venv_in_bookworm_disclaimer_rebuild": "Rebuilding the virtualenv will be attempted for the following apps (NB: the operation may take some time!): {rebuild_apps}",
"migration_0030_rebuild_python_venv_in_bookworm_failed": "Failed to rebuild the Python virtualenv for {app}. The app may not work as long as this is not resolved. You should fix the situation by forcing the upgrade of this app using `yunohost app upgrade --force {app}`.",
"migration_0030_rebuild_python_venv_in_bookworm_in_progress": "Now attempting to rebuild the Python virtualenv for `{app}`",
"migration_description_0021_migrate_to_bullseye": "Upgrade the system to Debian Bullseye and YunoHost 11.x",
"migration_description_0022_php73_to_php74_pools": "Migrate php7.3-fpm 'pool' conf files to php7.4",
"migration_description_0023_postgresql_11_to_13": "Migrate databases from PostgreSQL 11 to 13",
"migration_description_0024_rebuild_python_venv": "Repair Python app after bullseye migration",
"migration_description_0025_global_settings_to_configpanel": "Migrate legacy global settings nomenclature to the new, modern nomenclature",
"migration_description_0026_new_admins_group": "Migrate to the new 'multiple admins' system",
"migration_description_0027_migrate_to_bookworm": "Upgrade the system to Debian Bookworm and YunoHost 12",
"migration_description_0028_delete_legacy_xmpp_permission": "Delete the old XMPP permissions, Metronome is now an app",
"migration_description_0029_postgresql_13_to_15": "Migrate databases from PostgreSQL 13 to 15",
"migration_description_0030_rebuild_python_venv_in_bookworm": "Repair Python app after bookworm migration",
"migration_ldap_backup_before_migration": "Creating a backup of LDAP database and apps settings prior to the actual migration.",
"migration_ldap_can_not_backup_before_migration": "The backup of the system could not be completed before the migration failed. Error: {error}",
"migration_ldap_migration_failed_trying_to_rollback": "Could not migrate… trying to roll back the system.",
@ -664,7 +660,9 @@
"pattern_domain": "Must be a valid domain name (e.g. my-domain.org)",
"pattern_email": "Must be a valid e-mail address, without '+' symbol (e.g. someone@example.com)",
"pattern_email_forward": "Must be a valid e-mail address, '+' symbol accepted (e.g. someone+tag@example.com)",
"pattern_firstname": "Must be a valid first name (at least 3 chars)",
"pattern_fullname": "Must be a valid full name (at least 3 chars)",
"pattern_lastname": "Must be a valid last name (at least 3 chars)",
"pattern_mailbox_quota": "Must be a size with b/k/M/G/T suffix or 0 to not have a quota",
"pattern_password": "Must be at least 3 characters long",
"pattern_password_app": "Sorry, passwords can not contain the following characters: {forbidden_chars}",
@ -689,22 +687,7 @@
"port_already_closed": "Port {port} is already closed for {ip_version} connections",
"port_already_opened": "Port {port} is already opened for {ip_version} connections",
"postinstall_low_rootfsspace": "The root filesystem has a total space less than 10 GB, which is quite worrisome! You will likely run out of disk space very quickly! It's recommended to have at least 16GB for the root filesystem. If you want to install YunoHost despite this warning, re-run the postinstall with --force-diskspace",
"pydantic_type_error": "Invalid type.",
"pydantic_type_error_none_not_allowed": "Value is required.",
"pydantic_type_error_str": "Invalid type, string expected.",
"pydantic_value_error_color": "Not a valid color, value must be a named or hex color.",
"pydantic_value_error_const": "Unexpected value; choose between {permitted}",
"pydantic_value_error_date": "Invalid date format",
"pydantic_value_error_email": "Value is not a valid email address",
"pydantic_value_error_number_not_ge": "Value must be greater than or equal to {limit_value}.",
"pydantic_value_error_number_not_le": "Value must be less than or equal to {limit_value}.",
"pydantic_value_error_str_regex": "Invalid string; value doesn't respects the pattern '{pattern}'",
"pydantic_value_error_time": "Invalid time format",
"pydantic_value_error_url_extra": "URL invalid, extra characters found after valid URL: '{extra}'",
"pydantic_value_error_url_host": "URL host invalid",
"pydantic_value_error_url_port": "URL port invalid, port cannot exceed 65535",
"pydantic_value_error_url_scheme": "Invalid or missing URL scheme",
"regenconf_dry_pending_applying": "Checking pending configuration which would have been applied for category '{category}'...",
"regenconf_dry_pending_applying": "Checking pending configuration which would have been applied for category '{category}'…",
"regenconf_failed": "Could not regenerate the configuration for category(s): {categories}",
"regenconf_file_backed_up": "Configuration file '{conf}' backed up to '{backup}'",
"regenconf_file_copy_failed": "Could not copy the new configuration file '{new}' to '{conf}'",
@ -753,17 +736,17 @@
"service_description_dnsmasq": "Handles domain name resolution (DNS)",
"service_description_dovecot": "Allows e-mail clients to access/fetch email (via IMAP and POP3)",
"service_description_fail2ban": "Protects against brute-force and other kinds of attacks from the Internet",
"service_description_metronome": "Manage XMPP instant messaging accounts",
"service_description_mysql": "Stores app data (SQL database)",
"service_description_nginx": "Serves or provides access to all the websites hosted on your server",
"service_description_opendkim": "Signs outgoing emails using DKIM such that they are less likely to be flagged as spam",
"service_description_postfix": "Used to send and receive e-mails",
"service_description_postgresql": "Stores app data (SQL database)",
"service_description_redis-server": "A specialized database used for rapid data access, task queue, and communication between programs",
"service_description_rspamd": "Filters spam, and other e-mail related features",
"service_description_slapd": "Stores users, domains and related info",
"service_description_ssh": "Allows you to connect remotely to your server via a terminal (SSH protocol)",
"service_description_yunohost-api": "Manages interactions between the YunoHost web interface and the system",
"service_description_yunohost-firewall": "Manages open and close connection ports to services",
"service_description_yunohost-portal-api": "Manages interactions between the different user portal web interfaces and the system",
"service_description_yunomdns": "Allows you to reach your server using 'yunohost.local' in your local network",
"service_disable_failed": "Could not make the service '{service}' not start at boot.\n\nRecent service logs:{logs}",
"service_disabled": "The service '{service}' will not be started anymore when system boots.",
@ -785,7 +768,7 @@
"service_unknown": "Unknown service '{service}'",
"show_tile_cant_be_enabled_for_regex": "You cannot enable 'show_tile' right now, because the URL for the permission '{permission}' is a regex",
"show_tile_cant_be_enabled_for_url_not_defined": "You cannot enable 'show_tile' right now, because you must first define an URL for the permission '{permission}'",
"ssowat_conf_generated": "SSO and portal configurations regenerated",
"ssowat_conf_generated": "SSOwat configuration regenerated",
"system_upgraded": "System upgraded",
"system_username_exists": "Username already exists in the list of system users",
"this_action_broke_dpkg": "This action broke dpkg/APT (the system package managers)… You can try to solve this issue by connecting through SSH and running `sudo apt install --fix-broken` and/or `sudo dpkg --configure -a`.",

View file

@ -5,7 +5,7 @@ REPO_URL=$(git remote get-url origin)
ME=$(git config --get user.name)
EMAIL=$(git config --get user.email)
LAST_RELEASE=$(git tag --list 'debian/12.*' --sort="v:refname" | tail -n 1)
LAST_RELEASE=$(git tag --list 'debian/11.*' --sort="v:refname" | tail -n 1)
echo "$REPO ($VERSION) $RELEASE; urgency=low"
echo ""

View file

@ -76,6 +76,9 @@ def find_expected_string_keys():
continue
yield "migration_description_" + os.path.basename(path)[:-3]
# FIXME: to be removed in bookworm branch
yield "migration_description_0027_migrate_to_bookworm"
# For each default service, expect to find "service_description_<name>"
for service, info in yaml.safe_load(
open(ROOT + "conf/yunohost/services.yml")
@ -136,32 +139,16 @@ def find_expected_string_keys():
# Domain config panel
domain_config = toml.load(open(ROOT + "share/config_domain.toml"))
domain_settings_with_help_key = [
"portal_logo",
"portal_public_intro",
"portal_theme",
"portal_user_intro",
"search_engine",
"custom_css",
"dns",
"enable_public_apps_page",
]
domain_section_with_no_name = ["app", "cert_", "mail", "registrar"]
for panel_key, panel in domain_config.items():
for panel in domain_config.values():
if not isinstance(panel, dict):
continue
yield f"domain_config_{panel_key}_name"
for section_key, section in panel.items():
for section in panel.values():
if not isinstance(section, dict):
continue
if section_key not in domain_section_with_no_name:
yield f"domain_config_{section_key}_name"
for key, values in section.items():
if not isinstance(values, dict):
continue
yield f"domain_config_{key}"
if key in domain_settings_with_help_key:
yield f"domain_config_{key}_help"
# Global settings
global_config = toml.load(open(ROOT + "share/config_global.toml"))
@ -178,14 +165,12 @@ def find_expected_string_keys():
"root_password_confirm",
]
for panel_key, panel in global_config.items():
for panel in global_config.values():
if not isinstance(panel, dict):
continue
yield f"global_settings_setting_{panel_key}_name"
for section_key, section in panel.items():
for section in panel.values():
if not isinstance(section, dict):
continue
yield f"global_settings_setting_{section_key}_name"
for key, values in section.items():
if not isinstance(values, dict):
continue

View file

@ -1,92 +0,0 @@
_global:
namespace: yunohost
authentication:
api: ldap_ynhuser
cli: null
lock: false
cache: false
portal:
category_help: Portal routes
actions:
### portal_me()
me:
action_help: Allow user to fetch their own infos
api: GET /me
### portal_apps()
apps:
action_help: Allow users to fetch lit of apps they have access to
api: GET /me/apps
### portal_update()
update:
action_help: Allow user to update their infos (display name, mail aliases/forward, password, ...)
api: PUT /update
arguments:
--fullname:
help: The full name of the user. For example 'Camille Dupont'
extra:
pattern: &pattern_fullname
- !!str ^([^\W_]{1,30}[ ,.'-]{0,3})+$
- "pattern_fullname"
--mailforward:
help: Mailforward addresses to add
nargs: "*"
metavar: MAIL
extra:
pattern: &pattern_email_forward
- !!str ^[\w\+.-]+@([^\W_A-Z]+([-]*[^\W_A-Z]+)*\.)+((xn--)?[^\W_]{2,})$
- "pattern_email_forward"
--mailalias:
help: Mail aliases to add
nargs: "*"
metavar: MAIL
extra:
pattern: &pattern_email
- !!str ^[\w.-]+@([^\W_A-Z]+([-]*[^\W_A-Z]+)*\.)+((xn--)?[^\W_]{2,})$
- "pattern_email"
--currentpassword:
help: Current password
nargs: "?"
--newpassword:
help: New password to set
nargs: "?"
### portal_update_password()
# update_password:
# action_help: Allow user to change their password
# api: PUT /me/update_password
# arguments:
# -c:
# full: --current
# help: Current password
# -p:
# full: --password
# help: New password to set
### portal_reset_password()
reset_password:
action_help: Allow user to update their infos (display name, mail aliases/forward, ...)
api: PUT /me/reset_password
authentication:
# FIXME: to be implemented ?
api: reset_password_token
# FIXME: add args etc
### portal_register()
register:
action_help: Allow user to register using an invite token or ???
api: POST /me
authentication:
# FIXME: to be implemented ?
api: register_invite_token
# FIXME: add args etc
### portal_public()
public:
action_help: Allow anybody to list public apps and other infos regarding the public portal
api: GET /public
authentication:
api: null

View file

@ -70,10 +70,26 @@ user:
help: The full name of the user. For example 'Camille Dupont'
extra:
ask: ask_fullname
required: True
required: False
pattern: &pattern_fullname
- !!str ^([^\W_]{1,30}[ ,.'-]{0,3})+$
- "pattern_fullname"
-f:
full: --firstname
help: Deprecated. Use --fullname instead.
extra:
required: False
pattern: &pattern_firstname
- !!str ^([^\W\d_]{1,30}[ ,.'-]{0,3})+$
- "pattern_firstname"
-l:
full: --lastname
help: Deprecated. Use --fullname instead.
extra:
required: False
pattern: &pattern_lastname
- !!str ^([^\W\d_]{1,30}[ ,.'-]{0,3})+$
- "pattern_lastname"
-p:
full: --password
help: User password
@ -86,7 +102,7 @@ user:
comment: good_practices_about_user_password
-d:
full: --domain
help: Domain for the email address
help: Domain for the email address and xmpp account
extra:
pattern: &pattern_domain
- !!str ^([^\W_A-Z]+([-]*[^\W_A-Z]+)*\.)+((xn--)?[^\W_]{2,})$
@ -131,6 +147,16 @@ user:
help: The full name of the user. For example 'Camille Dupont'
extra:
pattern: *pattern_fullname
-f:
full: --firstname
help: Deprecated. Use --fullname instead.
extra:
pattern: *pattern_firstname
-l:
full: --lastname
help: Deprecated. Use --fullname instead.
extra:
pattern: *pattern_lastname
-m:
full: --mail
extra:
@ -218,6 +244,10 @@ user:
action_help: List existing groups
api: GET /users/groups
arguments:
-s:
full: --short
help: List only the names of groups
action: store_true
-f:
full: --full
help: Display all informations known about each groups
@ -463,7 +493,7 @@ domain:
help: Display domains as a tree
action: store_true
--features:
help: List only domains with features enabled (mail_in, mail_out)
help: List only domains with features enabled (xmpp, mail_in, mail_out)
nargs: "*"
### domain_info()
@ -524,6 +554,17 @@ domain:
extra:
pattern: *pattern_password
### domain_dns_conf()
dns-conf:
deprecated: true
action_help: Generate sample DNS configuration for a domain
arguments:
domain:
help: Target domain
extra:
pattern: *pattern_domain
### domain_maindomain()
main-domain:
action_help: Check the current main domain, or change it
@ -537,6 +578,54 @@ domain:
extra:
pattern: *pattern_domain
### certificate_status()
cert-status:
deprecated: true
action_help: List status of current certificates (all by default).
arguments:
domain_list:
help: Domains to check
nargs: "*"
--full:
help: Show more details
action: store_true
### certificate_install()
cert-install:
deprecated: true
action_help: Install Let's Encrypt certificates for given domains (all by default).
arguments:
domain_list:
help: Domains for which to install the certificates
nargs: "*"
--force:
help: Install even if current certificate is not self-signed
action: store_true
--no-checks:
help: Does not perform any check that your domain seems correctly configured (DNS, reachability) before attempting to install. (Not recommended)
action: store_true
--self-signed:
help: Install self-signed certificate instead of Let's Encrypt
action: store_true
### certificate_renew()
cert-renew:
deprecated: true
action_help: Renew the Let's Encrypt certificates for given domains (all by default).
arguments:
domain_list:
help: Domains for which to renew the certificates
nargs: "*"
--force:
help: Ignore the validity threshold (15 days)
action: store_true
--email:
help: Send an email to root with logs if some renewing fails
action: store_true
--no-checks:
help: Does not perform any check that your domain seems correctly configured (DNS, reachability) before attempting to renew. (Not recommended)
action: store_true
### domain_url_available()
url-available:
hide_in_help: True
@ -736,7 +825,7 @@ domain:
help: Domains for which to renew the certificates
nargs: "*"
--force:
help: Ignore the validity threshold (15 days)
help: Ignore the validity threshold (30 days)
action: store_true
--email:
help: Send an email to root with logs if some renewing fails
@ -1978,7 +2067,7 @@ diagnosis:
api: PUT /diagnosis/ignore
arguments:
--filter:
help: "Add a filter. The first element should be a diagnosis category, and other criterias can be provided using the infos from the 'meta' sections in 'yunohost diagnosis show'. For example: 'dnsrecords domain=yolo.test category=mail'"
help: "Add a filter. The first element should be a diagnosis category, and other criterias can be provided using the infos from the 'meta' sections in 'yunohost diagnosis show'. For example: 'dnsrecords domain=yolo.test category=xmpp'"
nargs: "*"
metavar: CRITERIA
--list:

View file

@ -2,8 +2,16 @@ version = "1.0"
i18n = "domain_config"
[feature]
name = "Features"
[feature.app]
[feature.app.default_app]
type = "app"
filter = "is_webapp"
default = "_none"
[feature.mail]
[feature.mail.mail_out]
type = "boolean"
default = 1
@ -12,106 +20,61 @@ i18n = "domain_config"
type = "boolean"
default = 1
[feature.app]
[feature.app.default_app]
type = "app"
filter = "is_webapp"
default = "_none"
[feature.xmpp]
[feature.portal]
# Only available for "topest" domains
[feature.portal.enable_public_apps_page]
[feature.xmpp.xmpp]
type = "boolean"
default = false
[feature.portal.show_other_domains_apps]
type = "boolean"
default = false
[feature.portal.portal_title]
type = "string"
default = "YunoHost"
[feature.portal.portal_logo]
type = "file"
accept = ["image/png", "image/jpeg", "image/svg+xml"]
mode = "python"
bind = "/usr/share/yunohost/portal/customassets/{filename}{ext}"
[feature.portal.portal_theme]
type = "select"
choices = ["system", "light", "dark", "omg", "legacy", "black", "synthwave", "halloween", "coffee", "cupcake", "cyberpunk", "valentine", "nord"]
default = "system"
[feature.portal.search_engine]
type = "url"
default = ""
[feature.portal.search_engine_name]
type = "string"
visible = "search_engine"
[feature.portal.portal_user_intro]
type = "text"
[feature.portal.portal_public_intro]
type = "text"
# FIXME link to GCU
[feature.portal.custom_css]
# NB: this is wrote into "/usr/share/yunohost/portal/customassets/{domain}.custom.css"
type = "text"
default = 0
[dns]
name = "DNS"
[dns.registrar]
# This part is automatically generated in DomainConfigPanel
[cert]
name = "Certificate"
[cert.cert_]
# The section has a different id than 'cert' otherwise it ends up with an unecessary "name" because it's defined for the panel (in i18n.json)
[cert.cert]
[cert.cert_.cert_summary]
[cert.cert.cert_summary]
type = "alert"
# Automatically filled by DomainConfigPanel
[cert.cert_.cert_validity]
[cert.cert.cert_validity]
type = "number"
readonly = true
visible = "false"
# Automatically filled by DomainConfigPanel
[cert.cert_.cert_issuer]
[cert.cert.cert_issuer]
type = "string"
visible = false
# Automatically filled by DomainConfigPanel
[cert.cert_.acme_eligible]
[cert.cert.acme_eligible]
type = "boolean"
visible = false
# Automatically filled by DomainConfigPanel
[cert.cert_.acme_eligible_explain]
[cert.cert.acme_eligible_explain]
type = "alert"
style = "warning"
visible = "acme_eligible == false || acme_eligible == null"
[cert.cert_.cert_no_checks]
[cert.cert.cert_no_checks]
type = "boolean"
default = false
visible = "acme_eligible == false || acme_eligible == null"
[cert.cert_.cert_install]
[cert.cert.cert_install]
type = "button"
icon = "star"
style = "success"
visible = "cert_issuer != 'letsencrypt'"
enabled = "acme_eligible || cert_no_checks"
[cert.cert_.cert_renew]
[cert.cert.cert_renew]
type = "button"
icon = "refresh"
style = "warning"

View file

@ -2,7 +2,9 @@ version = "1.0"
i18n = "global_settings_setting"
[security]
name = "Security"
[security.password]
name = "Passwords"
[security.password.admin_strength]
type = "select"
@ -26,7 +28,7 @@ i18n = "global_settings_setting"
default = false
[security.ssh]
name = "SSH"
[security.ssh.ssh_compatibility]
type = "select"
choices.intermediate = "Intermediate (compatible with older softwares)"
@ -42,6 +44,7 @@ i18n = "global_settings_setting"
default = true
[security.nginx]
name = "NGINX (web server)"
[security.nginx.nginx_redirect_to_https]
type = "boolean"
default = true
@ -53,7 +56,7 @@ i18n = "global_settings_setting"
default = "intermediate"
[security.postfix]
name = "Postfix (SMTP email server)"
[security.postfix.postfix_compatibility]
type = "select"
choices.intermediate = "Intermediate (allows TLS 1.2)"
@ -61,6 +64,7 @@ i18n = "global_settings_setting"
default = "intermediate"
[security.webadmin]
name = "Webadmin"
[security.webadmin.webadmin_allowlist_enabled]
type = "boolean"
default = false
@ -72,6 +76,8 @@ i18n = "global_settings_setting"
default = ""
[security.root_access]
name = "Change root password"
[security.root_access.root_access_explain]
type = "alert"
style = "info"
@ -88,17 +94,22 @@ i18n = "global_settings_setting"
default = ""
[security.experimental]
name = "Experimental"
[security.experimental.security_experimental_enabled]
type = "boolean"
default = false
[email]
name = "Email"
[email.pop3]
name = "POP3"
[email.pop3.pop3_enabled]
type = "boolean"
default = false
[email.smtp]
name = "SMTP"
[email.smtp.smtp_allow_ipv6]
type = "boolean"
default = true
@ -143,13 +154,26 @@ i18n = "global_settings_setting"
visible = "smtp_backup_mx_domains"
[misc]
name = "Other"
[misc.portal]
name = "User portal"
[misc.portal.ssowat_panel_overlay_enabled]
type = "boolean"
default = true
[misc.portal.portal_theme]
type = "select"
# Choices are loaded dynamically in the python code
default = "default"
[misc.backup]
name = "Backup"
[misc.backup.backup_compress_tar_archives]
type = "boolean"
default = false
[misc.network]
name = "Network"
[misc.network.dns_exposure]
type = "select"
choices.both = "Both"

View file

@ -50,13 +50,6 @@ def cli(debug, quiet, output_as, timeout, args, parser):
def api(debug, host, port):
allowed_cors_origins = []
allowed_cors_origins_file = "/etc/yunohost/.admin-api-allowed-cors-origins"
if os.path.exists(allowed_cors_origins_file):
allowed_cors_origins = open(allowed_cors_origins_file).read().strip().split(",")
init_logging(interface="api", debug=debug)
def is_installed_api():
@ -71,28 +64,6 @@ def api(debug, host, port):
actionsmap="/usr/share/yunohost/actionsmap.yml",
locales_dir="/usr/share/yunohost/locales/",
routes={("GET", "/installed"): is_installed_api},
allowed_cors_origins=allowed_cors_origins,
)
sys.exit(ret)
def portalapi(debug, host, port):
allowed_cors_origins = []
allowed_cors_origins_file = "/etc/yunohost/.portal-api-allowed-cors-origins"
if os.path.exists(allowed_cors_origins_file):
allowed_cors_origins = open(allowed_cors_origins_file).read().strip().split(",")
# FIXME : is this the logdir we want ? (yolo to work around permission issue)
init_logging(interface="portalapi", debug=debug, logdir="/var/log")
ret = moulinette.api(
host=host,
port=port,
actionsmap="/usr/share/yunohost/actionsmap-portal.yml",
locales_dir="/usr/share/yunohost/locales/",
allowed_cors_origins=allowed_cors_origins,
)
sys.exit(ret)
@ -144,11 +115,17 @@ def init_logging(interface="cli", debug=False, quiet=False, logdir="/var/log/yun
"version": 1,
"disable_existing_loggers": True,
"formatters": {
"tty-debug": {
"format": "%(relativeCreated)-4d %(level_with_color)s %(message)s"
"console": {
"format": "%(relativeCreated)-5d %(levelname)-8s %(name)s %(funcName)s - %(fmessage)s"
},
"tty-debug": {"format": "%(relativeCreated)-4d %(fmessage)s"},
"precise": {
"format": "%(asctime)-15s %(levelname)-8s %(name)s.%(funcName)s - %(message)s"
"format": "%(asctime)-15s %(levelname)-8s %(name)s %(funcName)s - %(fmessage)s"
},
},
"filters": {
"action": {
"()": "moulinette.utils.log.ActionFilter",
},
},
"handlers": {
@ -161,14 +138,11 @@ def init_logging(interface="cli", debug=False, quiet=False, logdir="/var/log/yun
"level": "DEBUG" if debug else "INFO",
"class": "moulinette.interfaces.api.APIQueueHandler",
},
"portalapi": {
"level": "DEBUG" if debug else "INFO",
"class": "moulinette.interfaces.api.APIQueueHandler",
},
"file": {
"class": "logging.FileHandler",
"formatter": "precise",
"filename": logfile,
"filters": ["action"],
},
},
"loggers": {
@ -190,7 +164,7 @@ def init_logging(interface="cli", debug=False, quiet=False, logdir="/var/log/yun
}
# Logging configuration for CLI (or any other interface than api...) #
if interface not in ["api", "portalapi"]:
if interface != "api":
configure_logging(logging_configuration)
# Logging configuration for API #

View file

@ -17,26 +17,26 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import time
import glob
import os
import shutil
import yaml
import time
import re
import subprocess
import tempfile
import copy
from typing import TYPE_CHECKING, List, Tuple, Dict, Any, Iterator, Optional, Union
from typing import List, Tuple, Dict, Any, Iterator, Optional
from packaging import version
from logging import getLogger
from pathlib import Path
from moulinette import Moulinette, m18n
from moulinette.utils.log import getActionLogger
from moulinette.utils.process import run_commands, check_output
from moulinette.utils.filesystem import (
read_file,
read_json,
read_toml,
read_yaml,
write_to_file,
write_to_json,
cp,
@ -45,6 +45,12 @@ from moulinette.utils.filesystem import (
chmod,
)
from yunohost.utils.configpanel import ConfigPanel, ask_questions_and_parse_answers
from yunohost.utils.form import (
DomainOption,
WebPathOption,
hydrate_questions_with_choices,
)
from yunohost.utils.i18n import _value_for_locale
from yunohost.utils.error import YunohostError, YunohostValidationError
from yunohost.utils.system import (
@ -65,13 +71,7 @@ from yunohost.app_catalog import ( # noqa
APPS_CATALOG_LOGOS,
)
if TYPE_CHECKING:
from pydantic.typing import AbstractSetIntStr, MappingIntStrAny
from yunohost.utils.configpanel import RawSettings
from yunohost.utils.form import FormModel
logger = getLogger("yunohost.app")
logger = getActionLogger("yunohost.app")
APPS_SETTING_PATH = "/etc/yunohost/apps/"
APP_TMP_WORKDIRS = "/var/cache/yunohost/app_tmp_work_dirs"
@ -96,8 +96,6 @@ APP_FILES_TO_COPY = [
"doc",
]
PORTAL_SETTINGS_DIR = "/etc/yunohost/portal"
def app_list(full=False, upgradable=False):
"""
@ -123,8 +121,8 @@ def app_info(app, full=False, upgradable=False):
"""
Get info for a specific app
"""
from yunohost.domain import _get_raw_domain_settings
from yunohost.permission import user_permission_list
from yunohost.domain import domain_config_get
_assert_is_installed(app)
@ -220,11 +218,11 @@ def app_info(app, full=False, upgradable=False):
rendered_notifications[name][lang] = rendered_content
ret["manifest"]["notifications"][step] = rendered_notifications
ret["is_webapp"] = "domain" in settings and settings["domain"] and "path" in settings
ret["is_webapp"] = "domain" in settings and "path" in settings
if ret["is_webapp"]:
ret["is_default"] = (
_get_raw_domain_settings(settings["domain"]).get("default_app") == app
domain_config_get(settings["domain"], "feature.app.default_app") == app
)
ret["supports_change_url"] = os.path.exists(
@ -257,8 +255,8 @@ def _app_upgradable(app_infos):
# Determine upgradability
app_in_catalog = app_infos.get("from_catalog")
installed_version = _parse_app_version(app_infos.get("version", "0~ynh0"))
version_in_catalog = _parse_app_version(
installed_version = version.parse(app_infos.get("version", "0~ynh0"))
version_in_catalog = version.parse(
app_infos.get("from_catalog", {}).get("manifest", {}).get("version", "0~ynh0")
)
@ -273,11 +271,29 @@ def _app_upgradable(app_infos):
):
return "bad_quality"
# If the app uses the standard version scheme, use it to determine
# upgradability
if "~ynh" in str(installed_version) and "~ynh" in str(version_in_catalog):
if installed_version < version_in_catalog:
return "yes"
else:
return "no"
# Legacy stuff for app with old / non-standard version numbers...
# In case there is neither update_time nor install_time, we assume the app can/has to be upgraded
if not app_infos["from_catalog"].get("lastUpdate") or not app_infos[
"from_catalog"
].get("git"):
return "url_required"
settings = app_infos["settings"]
local_update_time = settings.get("update_time", settings.get("install_time", 0))
if app_infos["from_catalog"]["lastUpdate"] > local_update_time:
return "yes"
else:
return "no"
def app_map(app=None, raw=False, user=None):
"""
@ -406,7 +422,6 @@ def app_change_url(operation_logger, app, domain, path):
path -- New path at which the application will be move
"""
from yunohost.utils.form import DomainOption, WebPathOption
from yunohost.hook import hook_exec_with_script_debug_if_failure, hook_callback
from yunohost.service import service_reload_or_restart
@ -558,7 +573,7 @@ def app_upgrade(
)
from yunohost.permission import permission_sync_to_user
from yunohost.regenconf import manually_modified_files
from yunohost.utils.legacy import _patch_legacy_helpers
from yunohost.utils.legacy import _patch_legacy_php_versions, _patch_legacy_helpers
from yunohost.backup import (
backup_list,
backup_create,
@ -619,11 +634,9 @@ def app_upgrade(
# Manage upgrade type and avoid any upgrade if there is nothing to do
upgrade_type = "UNKNOWN"
# Get current_version and new version
app_new_version_raw = manifest.get("version", "?")
app_current_version_raw = app_dict.get("version", "?")
app_new_version = _parse_app_version(app_new_version_raw)
app_current_version = _parse_app_version(app_current_version_raw)
if "~ynh" in str(app_current_version_raw) and "~ynh" in str(app_new_version_raw):
app_new_version = version.parse(manifest.get("version", "?"))
app_current_version = version.parse(app_dict.get("version", "?"))
if "~ynh" in str(app_current_version) and "~ynh" in str(app_new_version):
if app_current_version >= app_new_version and not force:
# In case of upgrade from file or custom repository
# No new version available
@ -643,8 +656,8 @@ def app_upgrade(
elif app_current_version == app_new_version:
upgrade_type = "UPGRADE_SAME"
else:
app_current_version_upstream, _ = str(app_current_version_raw).split("~ynh")
app_new_version_upstream, _ = str(app_new_version_raw).split("~ynh")
app_current_version_upstream, _ = str(app_current_version).split("~ynh")
app_new_version_upstream, _ = str(app_new_version).split("~ynh")
if app_current_version_upstream == app_new_version_upstream:
upgrade_type = "UPGRADE_PACKAGE"
else:
@ -671,7 +684,7 @@ def app_upgrade(
settings = _get_app_settings(app_instance_name)
notifications = _filter_and_hydrate_notifications(
manifest["notifications"]["PRE_UPGRADE"],
current_version=app_current_version_raw,
current_version=app_current_version,
data=settings,
)
_display_notifications(notifications, force=force)
@ -726,6 +739,9 @@ def app_upgrade(
# Attempt to patch legacy helpers ...
_patch_legacy_helpers(extracted_app_folder)
# Apply dirty patch to make php5 apps compatible with php7
_patch_legacy_php_versions(extracted_app_folder)
# Prepare env. var. to pass to script
env_dict = _make_environment_for_app_script(
app_instance_name, workdir=extracted_app_folder, action="upgrade"
@ -733,8 +749,8 @@ def app_upgrade(
env_dict_more = {
"YNH_APP_UPGRADE_TYPE": upgrade_type,
"YNH_APP_MANIFEST_VERSION": str(app_new_version_raw),
"YNH_APP_CURRENT_VERSION": str(app_current_version_raw),
"YNH_APP_MANIFEST_VERSION": str(app_new_version),
"YNH_APP_CURRENT_VERSION": str(app_current_version),
}
if manifest["packaging_format"] < 2:
@ -925,7 +941,7 @@ def app_upgrade(
settings = _get_app_settings(app_instance_name)
notifications = _filter_and_hydrate_notifications(
manifest["notifications"]["POST_UPGRADE"],
current_version=app_current_version_raw,
current_version=app_current_version,
data=settings,
)
if Moulinette.interface.type == "cli":
@ -960,11 +976,10 @@ def app_upgrade(
def app_manifest(app, with_screenshot=False):
from yunohost.utils.form import parse_raw_options
manifest, extracted_app_folder = _extract_app(app)
manifest["install"] = parse_raw_options(manifest.get("install", {}), serialize=True)
raw_questions = manifest.get("install", {}).values()
manifest["install"] = hydrate_questions_with_choices(raw_questions)
# Add a base64 image to be displayed in web-admin
if with_screenshot and Moulinette.interface.type == "api":
@ -1057,8 +1072,7 @@ def app_install(
permission_sync_to_user,
)
from yunohost.regenconf import manually_modified_files
from yunohost.utils.legacy import _patch_legacy_helpers
from yunohost.utils.form import ask_questions_and_parse_answers
from yunohost.utils.legacy import _patch_legacy_php_versions, _patch_legacy_helpers
from yunohost.user import user_list
# Check if disk space available
@ -1113,9 +1127,13 @@ def app_install(
app_setting_path = os.path.join(APPS_SETTING_PATH, app_instance_name)
# Retrieve arguments list for install script
raw_options = manifest["install"]
options, form = ask_questions_and_parse_answers(raw_options, prefilled_answers=args)
args = form.dict(exclude_none=True)
raw_questions = manifest["install"]
questions = ask_questions_and_parse_answers(raw_questions, prefilled_answers=args)
args = {
question.id: question.value
for question in questions
if not question.readonly and question.value is not None
}
# Validate domain / path availability for webapps
# (ideally this should be handled by the resource system for manifest v >= 2
@ -1126,6 +1144,9 @@ def app_install(
# Attempt to patch legacy helpers ...
_patch_legacy_helpers(extracted_app_folder)
# Apply dirty patch to make php5 apps compatible with php7
_patch_legacy_php_versions(extracted_app_folder)
# We'll check that the app didn't brutally edit some system configuration
manually_modified_files_before_install = manually_modified_files()
@ -1149,18 +1170,18 @@ def app_install(
"current_revision": manifest.get("remote", {}).get("revision", "?"),
}
# If packaging_format v2+, save all install options as settings
# If packaging_format v2+, save all install questions as settings
if packaging_format >= 2:
for option in options:
for question in questions:
# Except readonly "questions" that don't even have a value
if option.readonly:
if question.readonly:
continue
# Except user-provider passwords
# ... which we need to reinject later in the env_dict
if option.type == "password":
if question.type == "password":
continue
app_settings[option.id] = form[option.id]
app_settings[question.id] = question.value
_set_app_settings(app_instance_name, app_settings)
@ -1213,23 +1234,23 @@ def app_install(
app_instance_name, args=args, workdir=extracted_app_folder, action="install"
)
# If packaging_format v2+, save all install options as settings
# If packaging_format v2+, save all install questions as settings
if packaging_format >= 2:
for option in options:
for question in questions:
# Reinject user-provider passwords which are not in the app settings
# (cf a few line before)
if option.type == "password":
env_dict[option.id] = form[option.id]
if question.type == "password":
env_dict[question.id] = question.value
# We want to hav the env_dict in the log ... but not password values
env_dict_for_logging = env_dict.copy()
for option in options:
# Or should it be more generally option.redact ?
if option.type == "password":
if f"YNH_APP_ARG_{option.id.upper()}" in env_dict_for_logging:
del env_dict_for_logging[f"YNH_APP_ARG_{option.id.upper()}"]
if option.id in env_dict_for_logging:
del env_dict_for_logging[option.id]
for question in questions:
# Or should it be more generally question.redact ?
if question.type == "password":
if f"YNH_APP_ARG_{question.id.upper()}" in env_dict_for_logging:
del env_dict_for_logging[f"YNH_APP_ARG_{question.id.upper()}"]
if question.id in env_dict_for_logging:
del env_dict_for_logging[question.id]
operation_logger.extra.update({"env": env_dict_for_logging})
@ -1391,14 +1412,14 @@ def app_remove(operation_logger, app, purge=False, force_workdir=None):
purge -- Remove with all app data
force_workdir -- Special var to force the working directoy to use, in context such as remove-after-failed-upgrade or remove-after-failed-restore
"""
from yunohost.utils.legacy import _patch_legacy_helpers
from yunohost.utils.legacy import _patch_legacy_php_versions, _patch_legacy_helpers
from yunohost.hook import hook_exec, hook_remove, hook_callback
from yunohost.permission import (
user_permission_list,
permission_delete,
permission_sync_to_user,
)
from yunohost.domain import domain_list, domain_config_set, _get_raw_domain_settings
from yunohost.domain import domain_list, domain_config_get, domain_config_set
_assert_is_installed(app)
@ -1410,6 +1431,10 @@ def app_remove(operation_logger, app, purge=False, force_workdir=None):
# Attempt to patch legacy helpers ...
_patch_legacy_helpers(app_setting_path)
# Apply dirty patch to make php5 apps compatible with php7 (e.g. the remove
# script might date back from jessie install)
_patch_legacy_php_versions(app_setting_path)
if force_workdir:
# This is when e.g. calling app_remove() from the upgrade-failed case
# where we want to remove using the *new* remove script and not the old one
@ -1472,7 +1497,7 @@ def app_remove(operation_logger, app, purge=False, force_workdir=None):
hook_remove(app)
for domain in domain_list()["domains"]:
if _get_raw_domain_settings(domain).get("default_app") == app:
if domain_config_get(domain, "feature.app.default_app") == app:
domain_config_set(domain, "feature.app.default_app", "_none")
if ret == 0:
@ -1528,6 +1553,121 @@ def app_setting(app, key, value=None, delete=False):
"""
app_settings = _get_app_settings(app) or {}
#
# Legacy permission setting management
# (unprotected, protected, skipped_uri/regex)
#
is_legacy_permission_setting = any(
key.startswith(word + "_") for word in ["unprotected", "protected", "skipped"]
)
if is_legacy_permission_setting:
from yunohost.permission import (
user_permission_list,
user_permission_update,
permission_create,
permission_delete,
permission_url,
)
permissions = user_permission_list(full=True, apps=[app])["permissions"]
key_ = key.split("_")[0]
permission_name = f"{app}.legacy_{key_}_uris"
permission = permissions.get(permission_name)
# GET
if value is None and not delete:
return (
",".join(permission.get("uris", []) + permission["additional_urls"])
if permission
else None
)
# DELETE
if delete:
# If 'is_public' setting still exists, we interpret this as
# coming from a legacy app (because new apps shouldn't manage the
# is_public state themselves anymore...)
#
# In that case, we interpret the request for "deleting
# unprotected/skipped" setting as willing to make the app
# private
if (
"is_public" in app_settings
and "visitors" in permissions[app + ".main"]["allowed"]
):
if key.startswith("unprotected_") or key.startswith("skipped_"):
user_permission_update(app + ".main", remove="visitors")
if permission:
permission_delete(permission_name)
# SET
else:
urls = value
# If the request is about the root of the app (/), ( = the vast majority of cases)
# we interpret this as a change for the main permission
# (i.e. allowing/disallowing visitors)
if urls == "/":
if key.startswith("unprotected_") or key.startswith("skipped_"):
permission_url(app + ".main", url="/", sync_perm=False)
user_permission_update(app + ".main", add="visitors")
else:
user_permission_update(app + ".main", remove="visitors")
else:
urls = urls.split(",")
if key.endswith("_regex"):
urls = ["re:" + url for url in urls]
if permission:
# In case of new regex, save the urls, to add a new time in the additional_urls
# In case of new urls, we do the same thing but inversed
if key.endswith("_regex"):
# List of urls to save
current_urls_or_regex = [
url
for url in permission["additional_urls"]
if not url.startswith("re:")
]
else:
# List of regex to save
current_urls_or_regex = [
url
for url in permission["additional_urls"]
if url.startswith("re:")
]
new_urls = urls + current_urls_or_regex
# We need to clear urls because in the old setting the new setting override the old one and dont just add some urls
permission_url(permission_name, clear_urls=True, sync_perm=False)
permission_url(permission_name, add_url=new_urls)
else:
from yunohost.utils.legacy import legacy_permission_label
# Let's create a "special" permission for the legacy settings
permission_create(
permission=permission_name,
# FIXME find a way to limit to only the user allowed to the main permission
allowed=(
["all_users"]
if key.startswith("protected_")
else ["all_users", "visitors"]
),
url=None,
additional_urls=urls,
auth_header=not key.startswith("skipped_"),
label=legacy_permission_label(app, key.split("_")[0]),
show_tile=False,
protected=True,
)
return
#
# Regular setting management
#
# GET
if value is None and not delete:
return app_settings.get(key, None)
@ -1542,6 +1682,8 @@ def app_setting(app, key, value=None, delete=False):
# SET
else:
if key in ["redirected_urls", "redirected_regex"]:
value = yaml.safe_load(value)
app_settings[key] = value
_set_app_settings(app, app_settings)
@ -1573,7 +1715,6 @@ def app_register_url(app, domain, path):
domain -- The domain on which the app should be registered (e.g. your.domain.tld)
path -- The path to be registered (e.g. /coffee)
"""
from yunohost.utils.form import DomainOption, WebPathOption
from yunohost.permission import (
permission_url,
user_permission_update,
@ -1614,17 +1755,12 @@ def app_ssowatconf():
"""
from yunohost.domain import (
domain_list,
_get_raw_domain_settings,
_get_domain_portal_dict,
)
from yunohost.domain import domain_list, _get_maindomain, domain_config_get
from yunohost.permission import user_permission_list
from yunohost.settings import settings_get
domain_portal_dict = _get_domain_portal_dict()
main_domain = _get_maindomain()
domains = domain_list()["domains"]
portal_domains = domain_list(exclude_subdomains=True)["domains"]
all_permissions = user_permission_list(
full=True, ignore_system_perms=True, absolute_urls=True
)["permissions"]
@ -1632,26 +1768,49 @@ def app_ssowatconf():
permissions = {
"core_skipped": {
"users": [],
"label": "Core permissions - skipped",
"show_tile": False,
"auth_header": False,
"public": True,
"uris": [domain + "/yunohost/admin" for domain in domains]
+ [domain + "/yunohost/api" for domain in domains]
+ [domain + "/yunohost/portalapi" for domain in domains]
+ [
r"re:^[^/]*/502\.html$",
r"re:^[^/]*/\.well-known/ynh-diagnosis/.*$",
r"re:^[^/]*/\.well-known/acme-challenge/.*$",
r"re:^[^/]*/\.well-known/autoconfig/mail/config-v1\.1\.xml.*$",
"re:^[^/]/502%.html$",
"re:^[^/]*/%.well%-known/ynh%-diagnosis/.*$",
"re:^[^/]*/%.well%-known/acme%-challenge/.*$",
"re:^[^/]*/%.well%-known/autoconfig/mail/config%-v1%.1%.xml.*$",
],
}
}
# FIXME : this could be handled by nginx's regen conf to further simplify ssowat's code ...
redirected_regex = {
main_domain + r"/yunohost[\/]?$": "https://" + main_domain + "/yunohost/sso/"
}
redirected_urls = {}
for domain in domains:
default_app = _get_raw_domain_settings(domain).get("default_app")
if default_app not in ["_none", None] and _is_installed(default_app):
apps_using_remote_user_var_in_nginx = (
check_output(
"grep -nri '$remote_user' /etc/yunohost/apps/*/conf/*nginx*conf | awk -F/ '{print $5}' || true"
)
.strip()
.split("\n")
)
for app in _installed_apps():
app_settings = read_yaml(APPS_SETTING_PATH + app + "/settings.yml") or {}
# Redirected
redirected_urls.update(app_settings.get("redirected_urls", {}))
redirected_regex.update(app_settings.get("redirected_regex", {}))
from .utils.legacy import (
translate_legacy_default_app_in_ssowant_conf_json_persistent,
)
translate_legacy_default_app_in_ssowant_conf_json_persistent()
for domain in domains:
default_app = domain_config_get(domain, "feature.app.default_app")
if default_app != "_none" and _is_installed(default_app):
app_settings = _get_app_settings(default_app)
app_domain = app_settings["domain"]
app_path = app_settings["path"]
@ -1659,12 +1818,6 @@ def app_ssowatconf():
# Prevent infinite redirect loop...
if domain + "/" != app_domain + app_path:
redirected_urls[domain + "/"] = app_domain + app_path
elif bool(_get_raw_domain_settings(domain).get("enable_public_apps_page", False)):
redirected_urls[domain + "/"] = domain_portal_dict[domain]
# Will organize apps by portal domain
portal_domains_apps = {domain: {} for domain in portal_domains}
apps_catalog = _load_apps_catalog()["apps"]
# New permission system
for perm_name, perm_info in all_permissions.items():
@ -1679,98 +1832,38 @@ def app_ssowatconf():
continue
app_id = perm_name.split(".")[0]
app_settings = _get_app_settings(app_id)
if perm_info["auth_header"]:
if app_settings.get("auth_header"):
auth_header = app_settings.get("auth_header")
assert auth_header in ["basic-with-password", "basic-without-password"]
else:
auth_header = "basic-with-password"
else:
auth_header = False
permissions[perm_name] = {
"use_remote_user_var_in_nginx_conf": app_id
in apps_using_remote_user_var_in_nginx,
"users": perm_info["corresponding_users"],
"auth_header": auth_header,
"label": perm_info["label"],
"show_tile": perm_info["show_tile"]
and perm_info["url"]
and (not perm_info["url"].startswith("re:")),
"auth_header": perm_info["auth_header"],
"public": "visitors" in perm_info["allowed"],
"uris": uris,
}
# Apps can opt out of the auth spoofing protection using this if they really need to,
# but that's a huge security hole and ultimately should never happen...
# ... But some apps live caldav/webdav need this to not break external clients x_x
apps_that_need_external_auth_maybe = ["agendav", "baikal", "keeweb", "monica", "nextcloud", "paheko", "radicale", "tracim", "vikunja", "z-push"]
protect_against_basic_auth_spoofing = app_settings.get("protect_against_basic_auth_spoofing")
if protect_against_basic_auth_spoofing is not None:
permissions[perm_name]["protect_against_basic_auth_spoofing"] = protect_against_basic_auth_spoofing not in [False, "False", "false", "0", 0]
elif app_id in apps_that_need_external_auth_maybe:
permissions[perm_name]["protect_against_basic_auth_spoofing"] = False
# Next: portal related
# No need to keep apps that aren't supposed to be displayed in portal
if not perm_info.get("show_tile", False):
continue
setting_path = os.path.join(APPS_SETTING_PATH, app_id)
local_manifest = _get_manifest_of_app(setting_path)
app_domain = uris[0].split("/")[0]
# get "topest" domain
app_portal_domain = next(
domain for domain in portal_domains if domain in app_domain
)
app_portal_info = {
"label": perm_info["label"],
"users": perm_info["corresponding_users"],
"public": "visitors" in perm_info["allowed"],
"url": uris[0],
"description": local_manifest["description"],
}
# FIXME : find a smarter way to get this info ? (in the settings maybe..)
# Also ideally we should not rely on the webadmin route for this, maybe expose these through a different route in nginx idk
# Also related to "people will want to customize those.."
app_catalog_info = apps_catalog.get(app_id.split("__")[0])
if app_catalog_info and "logo_hash" in app_catalog_info:
app_portal_info["logo"] = f"/yunohost/sso/applogos/{app_catalog_info['logo_hash']}.png"
portal_domains_apps[app_portal_domain][perm_name] = app_portal_info
conf_dict = {
"cookie_secret_file": "/etc/yunohost/.ssowat_cookie_secret",
"session_folder": "/var/cache/yunohost-portal/sessions",
"cookie_name": "yunohost.portal",
"theme": settings_get("misc.portal.portal_theme"),
"portal_domain": main_domain,
"portal_path": "/yunohost/sso/",
"additional_headers": {
"Auth-User": "uid",
"Remote-User": "uid",
"Name": "cn",
"Email": "mail",
},
"domains": domains,
"redirected_urls": redirected_urls,
"domain_portal_urls": domain_portal_dict,
"redirected_regex": redirected_regex,
"permissions": permissions,
}
write_to_json("/etc/ssowat/conf.json", conf_dict, sort_keys=True, indent=4)
# Generate a file per possible portal with available apps
for domain, apps in portal_domains_apps.items():
portal_settings = {}
portal_settings_path = Path(PORTAL_SETTINGS_DIR) / f"{domain}.json"
if portal_settings_path.exists():
portal_settings.update(read_json(str(portal_settings_path)))
# Do no override anything else than "apps" since the file is shared
# with domain's config panel "portal" options
portal_settings["apps"] = apps
write_to_json(
str(portal_settings_path), portal_settings, sort_keys=True, indent=4
)
# Cleanup old files from possibly old domains
for setting_file in Path(PORTAL_SETTINGS_DIR).iterdir():
if setting_file.name.endswith(".json"):
domain = setting_file.name[:-len(".json")]
if domain not in portal_domains_apps:
setting_file.unlink()
logger.debug(m18n.n("ssowat_conf_generated"))
@ -1791,13 +1884,11 @@ def app_change_label(app, new_label):
def app_action_list(app):
AppConfigPanel = _get_AppConfigPanel()
return AppConfigPanel(app).list_actions()
@is_unit_operation()
def app_action_run(operation_logger, app, action, args=None, args_file=None):
AppConfigPanel = _get_AppConfigPanel()
return AppConfigPanel(app).run_action(
action, args=args, args_file=args_file, operation_logger=operation_logger
)
@ -1819,7 +1910,6 @@ def app_config_get(app, key="", full=False, export=False):
else:
mode = "classic"
AppConfigPanel = _get_AppConfigPanel()
try:
config_ = AppConfigPanel(app)
return config_.get(key, mode)
@ -1839,52 +1929,39 @@ def app_config_set(
Apply a new app configuration
"""
AppConfigPanel = _get_AppConfigPanel()
config_ = AppConfigPanel(app)
return config_.set(key, value, args, args_file, operation_logger=operation_logger)
def _get_AppConfigPanel():
from yunohost.utils.configpanel import ConfigPanel
class AppConfigPanel(ConfigPanel):
entity_type = "app"
save_path_tpl = os.path.join(APPS_SETTING_PATH, "{entity}/settings.yml")
config_path_tpl = os.path.join(APPS_SETTING_PATH, "{entity}/config_panel.toml")
settings_must_be_defined: bool = True
def _get_raw_settings(self) -> "RawSettings":
return self._call_config_script("show")
def _run_action(self, action):
env = {key: str(value) for key, value in self.new_values.items()}
self._call_config_script(action, env=env)
def _apply(
self,
form: "FormModel",
previous_settings: dict[str, Any],
exclude: Union["AbstractSetIntStr", "MappingIntStrAny", None] = None,
) -> None:
env = {key: str(value) for key, value in form.dict().items()}
def _get_raw_settings(self):
self.values = self._call_config_script("show")
def _apply(self):
env = {key: str(value) for key, value in self.new_values.items()}
return_content = self._call_config_script("apply", env=env)
# If the script returned validation error
# raise a ValidationError exception using
# the first key
errors = return_content.get("validation_errors")
if errors:
for key, message in errors.items():
if return_content:
for key, message in return_content.get("validation_errors").items():
raise YunohostValidationError(
"app_argument_invalid",
name=key,
error=message,
)
def _run_action(self, form: "FormModel", action_id: str) -> None:
env = {key: str(value) for key, value in form.dict().items()}
self._call_config_script(action_id, env=env)
def _call_config_script(
self, action: str, env: Union[dict[str, Any], None] = None
) -> dict[str, Any]:
def _call_config_script(self, action, env=None):
from yunohost.hook import hook_exec
if env is None:
@ -1906,12 +1983,31 @@ ynh_app_config_run $1
# Call config script to extract current values
logger.debug(f"Calling '{action}' action from config script")
app = self.entity
app_id, app_instance_nb = _parse_app_instance_name(app)
settings = _get_app_settings(app)
app_setting_path = os.path.join(APPS_SETTING_PATH, self.entity)
app_script_env = _make_environment_for_app_script(app, workdir=app_setting_path)
manifest = _get_manifest_of_app(app_setting_path)
# FIXME: this is inconsistent with other script call ...
# this should be based on _make_environment_for_app_script ...
env.update(
{
"app_id": app_id,
"app": app,
"app_instance_nb": str(app_instance_nb),
"final_path": settings.get("final_path", ""),
"install_dir": settings.get("install_dir", ""),
"YNH_APP_BASEDIR": os.path.join(APPS_SETTING_PATH, app),
"YNH_APP_PACKAGING_FORMAT": str(manifest["packaging_format"]),
"YNH_APP_CONFIG_PANEL_OPTIONS_TYPES_AND_BINDS": self._dump_options_types_and_binds(),
}
)
app_script_env = _make_environment_for_app_script(app)
# Note that we only need to update settings wich are not already set
# The settings from config panel should be keep as it is
app_script_env.update(env)
app_script_env["YNH_APP_CONFIG_PANEL_OPTIONS_TYPES_AND_BINDS"] = self._dump_options_types_and_binds()
env = app_script_env
ret, values = hook_exec(config_script, args=[action], env=app_script_env)
ret, values = hook_exec(config_script, args=[action], env=env)
if ret != 0:
if action == "show":
raise YunohostError("app_config_unable_to_read")
@ -1921,15 +2017,15 @@ ynh_app_config_run $1
raise YunohostError("app_action_failed", action=action, app=app)
return values
def _get_partial_raw_config(self):
def _get_config_panel(self):
raw_config = super()._get_partial_raw_config()
ret = super()._get_config_panel()
self._compute_binds(raw_config)
self._compute_binds()
return raw_config
return ret
def _compute_binds(self, raw_config):
def _compute_binds(self):
"""
This compute the 'bind' statement for every option
In particular to handle __FOOBAR__ syntax
@ -1938,13 +2034,10 @@ ynh_app_config_run $1
settings = _get_app_settings(self.entity)
for panel_id, panel in raw_config.items():
if not isinstance(panel, dict):
continue
for panel, section, option in self._iterate():
bind_panel = panel.get("bind")
for section_id, section in panel.items():
if not isinstance(section, dict):
continue
bind_section = section.get("bind")
if not bind_section:
bind_section = bind_panel
@ -1954,9 +2047,7 @@ ynh_app_config_run $1
bind_section = bind_section + bind_panel_file
else:
bind_section = selector + bind_section + bind_panel_file
for option_id, option in section.items():
if not isinstance(option, dict):
continue
bind = option.get("bind")
if not bind:
if bind_section:
@ -1971,30 +2062,17 @@ ynh_app_config_run $1
bind = selector + bind + bind_file
if bind == "settings" and option.get("type", "string") == "file":
bind = "null"
if option.get("type", "string") == "button":
bind = "null"
option["bind"] = _hydrate_app_template(bind, settings)
def _dump_options_types_and_binds(self):
raw_config = self._get_partial_raw_config()
lines = []
for panel_id, panel in raw_config.items():
if not isinstance(panel, dict):
continue
for section_id, section in panel.items():
if not isinstance(section, dict):
continue
for option_id, option in section.items():
if not isinstance(option, dict):
continue
for _, _, option in self._iterate():
lines.append(
"|".join([option_id, option.get("type", "string"), option["bind"]])
"|".join([option["id"], option.get("type", "string"), option["bind"]])
)
return "\n".join(lines)
return AppConfigPanel
app_settings_cache: Dict[str, Dict[str, Any]] = {}
app_settings_cache_timestamp: Dict[str, float] = {}
@ -2038,6 +2116,20 @@ def _get_app_settings(app: str) -> Dict[str, Any]:
logger.error(m18n.n("app_not_correctly_installed", app=app))
return {}
# Stupid fix for legacy bullshit
# In the past, some setups did not have proper normalization for app domain/path
# Meaning some setups (as of January 2021) still have path=/foobar/ (with a trailing slash)
# resulting in stupid issue unless apps using ynh_app_normalize_path_stuff
# So we yolofix the settings if such an issue is found >_>
# A simple call to `yunohost app list` (which happens quite often) should be enough
# to migrate all app settings ... so this can probably be removed once we're past Bullseye...
if settings.get("path") != "/" and (
settings.get("path", "").endswith("/")
or not settings.get("path", "/").startswith("/")
):
settings["path"] = "/" + settings["path"].strip("/")
_set_app_settings(app, settings)
# Make the app id available as $app too
settings["app"] = app
@ -2073,20 +2165,6 @@ def _set_app_settings(app, settings):
del app_settings_cache[app]
def _parse_app_version(v):
if v in ["?", "-"]:
return (0, 0)
try:
if "~" in v:
return (version.parse(v.split("~")[0]), int(v.split("~")[1].replace("ynh", "")))
else:
return (version.parse(v), 0)
except Exception as e:
raise YunohostError(f"Failed to parse app version '{v}' : {e}", raw_msg=True)
def _get_manifest_of_app(path):
"Get app manifest stored in json or in toml"
@ -2909,7 +2987,6 @@ def _get_conflicting_apps(domain, path, ignore_app=None):
"""
from yunohost.domain import _assert_domain_exists
from yunohost.utils.form import DomainOption, WebPathOption
domain = DomainOption.normalize(domain)
path = WebPathOption.normalize(path)
@ -2966,7 +3043,6 @@ def _make_environment_for_app_script(
app_id, app_instance_nb = _parse_app_instance_name(app)
env_dict = {
"YNH_DEFAULT_PHP_VERSION": "8.2",
"YNH_APP_ID": app_id,
"YNH_APP_INSTANCE_NAME": app,
"YNH_APP_INSTANCE_NUMBER": str(app_instance_nb),
@ -3131,12 +3207,27 @@ def _assert_system_is_sane_for_app(manifest, when):
logger.debug("Checking that required services are up and running...")
# FIXME: in the past we had more elaborate checks about mariadb/php/postfix
# though they werent very formalized. Ideally we should rework this in the
# context of packaging v2, which implies deriving what services are
# relevant to check from the manifst
services = manifest.get("services", [])
services = ["nginx", "fail2ban"]
# Some apps use php-fpm, php5-fpm or php7.x-fpm which is now php7.4-fpm
def replace_alias(service):
if service in ["php-fpm", "php5-fpm", "php7.0-fpm", "php7.3-fpm"]:
return "php7.4-fpm"
else:
return service
services = [replace_alias(s) for s in services]
# We only check those, mostly to ignore "custom" services
# (added by apps) and because those are the most popular
# services
service_filter = ["nginx", "php7.4-fpm", "mysql", "postfix"]
services = [str(s) for s in services if s in service_filter]
if "nginx" not in services:
services = ["nginx"] + services
if "fail2ban" not in services:
services.append("fail2ban")
# Wait if a service is reloading
test_nb = 0
@ -3206,7 +3297,12 @@ def _notification_is_dismissed(name, settings):
def _filter_and_hydrate_notifications(notifications, current_version=None, data={}):
def is_version_more_recent_than_current_version(name, current_version):
current_version = str(current_version)
return _parse_app_version(name) > _parse_app_version(current_version)
# Boring code to handle the fact that "0.1 < 9999~ynh1" is False
if "~" in name:
return version.parse(name) > version.parse(current_version)
else:
return version.parse(name) > version.parse(current_version.split("~")[0])
out = {
# Should we render the markdown maybe? idk
@ -3285,7 +3381,7 @@ def regen_mail_app_user_config_for_dovecot_and_postfix(only=None):
dovecot = True if only in [None, "dovecot"] else False
postfix = True if only in [None, "postfix"] else False
from yunohost.utils.password import _hash_user_password
from yunohost.user import _hash_user_password
postfix_map = []
dovecot_passwd = []

View file

@ -19,28 +19,28 @@
import os
import re
import hashlib
from logging import getLogger
from moulinette import m18n
from moulinette.utils.log import getActionLogger
from moulinette.utils.network import download_json
from moulinette.utils.filesystem import (
read_json,
read_yaml,
write_to_json,
write_to_yaml,
mkdir,
)
from yunohost.utils.i18n import _value_for_locale
from yunohost.utils.error import YunohostError
logger = getLogger("yunohost.app_catalog")
logger = getActionLogger("yunohost.app_catalog")
APPS_CATALOG_CACHE = "/var/cache/yunohost/repo"
APPS_CATALOG_LOGOS = "/usr/share/yunohost/applogos"
APPS_CATALOG_CONF = "/etc/yunohost/apps_catalog.yml"
APPS_CATALOG_API_VERSION = 3
APPS_CATALOG_DEFAULT_URL = "https://app.yunohost.org/default"
DEFAULT_APPS_CATALOG_LIST = [{"id": "default", "url": APPS_CATALOG_DEFAULT_URL}]
def app_catalog(full=False, with_categories=False, with_antifeatures=False):
@ -120,21 +120,33 @@ def app_search(string):
return matching_apps
def _initialize_apps_catalog_system():
"""
This function is meant to intialize the apps_catalog system with YunoHost's default app catalog.
"""
default_apps_catalog_list = [{"id": "default", "url": APPS_CATALOG_DEFAULT_URL}]
try:
logger.debug(
"Initializing apps catalog system with YunoHost's default app list"
)
write_to_yaml(APPS_CATALOG_CONF, default_apps_catalog_list)
except Exception as e:
raise YunohostError(
f"Could not initialize the apps catalog system... : {e}", raw_msg=True
)
logger.success(m18n.n("apps_catalog_init_success"))
def _read_apps_catalog_list():
"""
Read the json corresponding to the list of apps catalogs
"""
if not os.path.exists(APPS_CATALOG_CONF):
return DEFAULT_APPS_CATALOG_LIST
try:
list_ = read_yaml(APPS_CATALOG_CONF)
if list_ == DEFAULT_APPS_CATALOG_LIST:
try:
os.remove(APPS_CATALOG_CONF)
except Exception:
pass
# Support the case where file exists but is empty
# by returning [] if list_ is None
return list_ if list_ else []

View file

@ -16,14 +16,11 @@
# 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 jwt
import os
import logging
import ldap
import ldap.sasl
import time
import hashlib
from pathlib import Path
from moulinette import m18n
from moulinette.authentication import BaseAuthenticator
@ -32,32 +29,14 @@ from moulinette.utils.text import random_ascii
from yunohost.utils.error import YunohostError, YunohostAuthenticationError
from yunohost.utils.ldap import _get_ldap_interface
session_secret = random_ascii()
logger = logging.getLogger("yunohost.authenticators.ldap_admin")
def SESSION_SECRET():
# Only load this once actually requested to avoid boring issues like
# "secret doesnt exists yet" (before postinstall) and therefore service
# miserably fail to start
if not SESSION_SECRET.value:
SESSION_SECRET.value = open("/etc/yunohost/.admin_cookie_secret").read().strip()
assert SESSION_SECRET.value
return SESSION_SECRET.value
SESSION_SECRET.value = None # type: ignore
SESSION_FOLDER = "/var/cache/yunohost/sessions"
SESSION_VALIDITY = 3 * 24 * 3600 # 3 days
LDAP_URI = "ldap://localhost:389"
ADMIN_GROUP = "cn=admins,ou=groups"
AUTH_DN = "uid={uid},ou=users,dc=yunohost,dc=org"
def short_hash(data):
return hashlib.shake_256(data.encode()).hexdigest(20)
class Authenticator(BaseAuthenticator):
name = "ldap_admin"
@ -143,87 +122,55 @@ class Authenticator(BaseAuthenticator):
if con:
con.unbind_s()
return {"user": uid}
def set_session_cookie(self, infos):
from bottle import response
assert isinstance(infos, dict)
assert "user" in infos
# Create a session id, built as <user_hash> + some random ascii
# Prefixing with the user hash is meant to provide the ability to invalidate all this user's session
# (eg because the user gets deleted, or password gets changed)
# User hashing not really meant for security, just to sort of anonymize/pseudonymize the session file name
infos["id"] = short_hash(infos['user']) + random_ascii(20)
# This allows to generate a new session id or keep the existing one
current_infos = self.get_session_cookie(raise_if_no_session_exists=False)
new_infos = {"id": current_infos["id"]}
new_infos.update(infos)
response.set_cookie(
"yunohost.admin",
jwt.encode(infos, SESSION_SECRET(), algorithm="HS256"),
new_infos,
secure=True,
secret=session_secret,
httponly=True,
path="/yunohost/api",
samesite="strict",
# samesite="strict", # Bottle 0.12 doesn't support samesite, to be added in next versions
)
# Create the session file (expiration mechanism)
session_file = f'{SESSION_FOLDER}/{infos["id"]}'
os.system(f'touch "{session_file}"')
def get_session_cookie(self, raise_if_no_session_exists=True):
from bottle import request, response
from bottle import request
try:
token = request.get_cookie("yunohost.admin", default="").encode()
infos = jwt.decode(
token,
SESSION_SECRET(),
algorithms="HS256",
options={"require": ["id", "user"]},
# N.B. : here we implicitly reauthenticate the cookie
# because it's signed via the session_secret
# If no session exists (or if session is invalid?)
# it's gonna return the default empty dict,
# which we interpret as an authentication failure
infos = request.get_cookie(
"yunohost.admin", secret=session_secret, default={}
)
except Exception:
if not raise_if_no_session_exists:
return {"id": random_ascii()}
raise YunohostAuthenticationError("unable_authenticate")
if not infos:
if not infos and raise_if_no_session_exists:
raise YunohostAuthenticationError("unable_authenticate")
self.purge_expired_session_files()
session_file = f'{SESSION_FOLDER}/{infos["id"]}'
if not os.path.exists(session_file):
response.delete_cookie("yunohost.admin", path="/yunohost/api")
raise YunohostAuthenticationError("session_expired")
if "id" not in infos:
infos["id"] = random_ascii()
# Otherwise, we 'touch' the file to extend the validity
os.system(f'touch "{session_file}"')
# FIXME: Here, maybe we want to re-authenticate the session via the authenticator
# For example to check that the username authenticated is still in the admin group...
return infos
def delete_session_cookie(self):
from bottle import response
try:
infos = self.get_session_cookie()
session_file = f'{SESSION_FOLDER}/{infos["id"]}'
os.remove(session_file)
except Exception as e:
logger.debug(f"User logged out, but failed to properly invalidate the session : {e}")
response.delete_cookie("yunohost.admin", path="/yunohost/api")
def purge_expired_session_files(self):
for session_file in Path(SESSION_FOLDER).iterdir():
if abs(session_file.stat().st_mtime - time.time()) > SESSION_VALIDITY:
try:
session_file.unlink()
except Exception as e:
logger.debug(f"Failed to delete session file {session_file} ? {e}")
@staticmethod
def invalidate_all_sessions_for_user(user):
for file in Path(SESSION_FOLDER).glob(f"{short_hash(user)}*"):
try:
file.unlink()
except Exception as e:
logger.debug(f"Failed to delete session file {file} ? {e}")
response.set_cookie("yunohost.admin", "", max_age=-1)
response.delete_cookie("yunohost.admin")

View file

@ -1,308 +0,0 @@
# -*- coding: utf-8 -*-
import time
import jwt
import logging
import ldap
import ldap.sasl
import base64
import os
import hashlib
from pathlib import Path
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives import padding
from cryptography.hazmat.backends import default_backend
from moulinette import m18n
from moulinette.authentication import BaseAuthenticator
from moulinette.utils.text import random_ascii
from moulinette.utils.filesystem import read_json
from yunohost.utils.error import YunohostError, YunohostAuthenticationError
from yunohost.utils.ldap import _get_ldap_interface
logger = logging.getLogger("yunohostportal.authenticators.ldap_ynhuser")
def SESSION_SECRET():
# Only load this once actually requested to avoid boring issues like
# "secret doesnt exists yet" (before postinstall) and therefore service
# miserably fail to start
if not SESSION_SECRET.value:
SESSION_SECRET.value = open("/etc/yunohost/.ssowat_cookie_secret").read().strip()
assert SESSION_SECRET.value
return SESSION_SECRET.value
SESSION_SECRET.value = None # type: ignore
SESSION_FOLDER = "/var/cache/yunohost-portal/sessions"
SESSION_VALIDITY = 3 * 24 * 3600 # 3 days
URI = "ldap://localhost:389"
USERDN = "uid={username},ou=users,dc=yunohost,dc=org"
# Cache on-disk settings to RAM for faster access
DOMAIN_USER_ACL_DICT: dict[str, dict] = {}
PORTAL_SETTINGS_DIR = "/etc/yunohost/portal"
# Should a user have *minimal* access to a domain?
# - if the user has permission for an application with a URI on the domain, yes
# - if the user is an admin, yes
# - if the user has an email on the domain, yes
# - otherwise, no
def user_is_allowed_on_domain(user: str, domain: str) -> bool:
assert "/" not in domain
portal_settings_path = Path(PORTAL_SETTINGS_DIR) / f"{domain}.json"
if not portal_settings_path.exists():
if "." not in domain:
return False
parent_domain = domain.split(".", 1)[-1]
return user_is_allowed_on_domain(user, parent_domain)
# Check that the domain permissions haven't changed on-disk since we read them
# by comparing file mtime. If we haven't read the file yet, read it for the first time.
# We compare mtime by equality not superiority because maybe the system clock has changed.
mtime = portal_settings_path.stat().st_mtime
if domain not in DOMAIN_USER_ACL_DICT or DOMAIN_USER_ACL_DICT[domain]["mtime"] != mtime:
users: set[str] = set()
for infos in read_json(str(portal_settings_path))["apps"].values():
users = users.union(infos["users"])
DOMAIN_USER_ACL_DICT[domain] = {}
DOMAIN_USER_ACL_DICT[domain]["mtime"] = mtime
DOMAIN_USER_ACL_DICT[domain]["users"] = users
if user in DOMAIN_USER_ACL_DICT[domain]["users"]:
# A user with explicit permission to an application is certainly welcome
return True
ADMIN_GROUP = "cn=admins,ou=groups"
try:
admins = (
_get_ldap_interface()
.search(ADMIN_GROUP, attrs=["memberUid"])[0]
.get("memberUid", [])
)
except Exception as e:
logger.error(f"Failed to list admin users: {e}")
return False
if user in admins:
# Admins can access everything
return True
try:
user_result = _get_ldap_interface().search("ou=users", f"uid={user}", [ "mail" ])
if len(user_result) != 1:
logger.error(f"User not found or many users found for {user}. How is this possible after so much validation?")
return False
user_mail = user_result[0]["mail"]
if len(user_mail) != 1:
logger.error(f"User {user} found, but has the wrong number of email addresses: {user_mail}")
return False
user_mail = user_mail[0]
if not "@" in user_mail:
logger.error(f"Invalid email address for {user}: {user_mail}")
return False
if user_mail.split("@")[1] == domain:
# A user from that domain is welcome
return True
# Users from other domains don't belong here
return False
except Exception as e:
logger.error(f"Failed to get email info for {user}: {e}")
return False
# We want to save the password in the cookie, but we should do so in an encrypted fashion
# This is needed because the SSO later needs to possibly inject the Basic Auth header
# which includes the user's password
# It's also needed because we need to be able to open LDAP sessions, authenticated as the user,
# which requires the user's password
#
# To do so, we use AES-256-CBC. As it's a block encryption algorithm, it requires an IV,
# which we need to keep around for decryption on SSOwat'side.
#
# SESSION_SECRET is used as the encryption key, which implies it must be exactly 32-char long (256/8)
#
# The result is a string formatted as <password_enc_b64>|<iv_b64>
# For example: ctl8kk5GevYdaA5VZ2S88Q==|yTAzCx0Gd1+MCit4EQl9lA==
def encrypt(data):
alg = algorithms.AES(SESSION_SECRET().encode())
iv = os.urandom(int(alg.block_size / 8))
E = Cipher(alg, modes.CBC(iv), default_backend()).encryptor()
p = padding.PKCS7(alg.block_size).padder()
data_padded = p.update(data.encode()) + p.finalize()
data_enc = E.update(data_padded) + E.finalize()
data_enc_b64 = base64.b64encode(data_enc).decode()
iv_b64 = base64.b64encode(iv).decode()
return data_enc_b64 + "|" + iv_b64
def decrypt(data_enc_and_iv_b64):
data_enc_b64, iv_b64 = data_enc_and_iv_b64.split("|")
data_enc = base64.b64decode(data_enc_b64)
iv = base64.b64decode(iv_b64)
alg = algorithms.AES(SESSION_SECRET().encode())
D = Cipher(alg, modes.CBC(iv), default_backend()).decryptor()
p = padding.PKCS7(alg.block_size).unpadder()
data_padded = D.update(data_enc)
data = p.update(data_padded) + p.finalize()
return data.decode()
def short_hash(data):
return hashlib.shake_256(data.encode()).hexdigest(20)
class Authenticator(BaseAuthenticator):
name = "ldap_ynhuser"
def _authenticate_credentials(self, credentials=None):
from bottle import request
try:
username, password = credentials.split(":", 1)
except ValueError:
raise YunohostError("invalid_credentials")
def _reconnect():
con = ldap.ldapobject.ReconnectLDAPObject(URI, retry_max=2, retry_delay=0.5)
con.simple_bind_s(USERDN.format(username=username), password)
return con
try:
con = _reconnect()
except ldap.INVALID_CREDENTIALS:
# FIXME FIXME FIXME : this should be properly logged and caught by Fail2ban ! ! ! ! ! ! !
raise YunohostError("invalid_password")
except ldap.SERVER_DOWN:
logger.warning(m18n.n("ldap_server_down"))
# Check that we are indeed logged in with the expected identity
try:
# whoami_s return dn:..., then delete these 3 characters
who = con.whoami_s()[3:]
except Exception as e:
logger.warning("Error during ldap authentication process: %s", e)
raise
else:
if who != USERDN.format(username=username):
raise YunohostError(
"Not logged with the appropriate identity ?!",
raw_msg=True,
)
finally:
# Free the connection, we don't really need it to keep it open as the point is only to check authentication...
if con:
con.unbind_s()
if not user_is_allowed_on_domain(username, request.get_header("host")):
raise YunohostAuthenticationError("unable_authenticate")
return {"user": username, "pwd": encrypt(password)}
def set_session_cookie(self, infos):
from bottle import response, request
assert isinstance(infos, dict)
assert "user" in infos
assert "pwd" in infos
# Create a session id, built as <user_hash> + some random ascii
# Prefixing with the user hash is meant to provide the ability to invalidate all this user's session
# (eg because the user gets deleted, or password gets changed)
# User hashing not really meant for security, just to sort of anonymize/pseudonymize the session file name
infos["id"] = short_hash(infos['user']) + random_ascii(20)
infos["host"] = request.get_header("host")
is_dev = Path("/etc/yunohost/.portal-api-allowed-cors-origins").exists()
response.set_cookie(
"yunohost.portal",
jwt.encode(infos, SESSION_SECRET(), algorithm="HS256"),
secure=True,
httponly=True,
path="/",
# Doesn't this cause issues ? May cause issue if the portal is on different subdomain than the portal API ? Will surely cause issue for development similar to CORS ?
samesite="strict" if not is_dev else None,
domain=f".{request.get_header('host')}",
)
# Create the session file (expiration mechanism)
session_file = f'{SESSION_FOLDER}/{infos["id"]}'
os.system(f'touch "{session_file}"')
def get_session_cookie(self, decrypt_pwd=False):
from bottle import request, response
try:
token = request.get_cookie("yunohost.portal", default="").encode()
infos = jwt.decode(
token,
SESSION_SECRET(),
algorithms="HS256",
options={"require": ["id", "host", "user", "pwd"]},
)
except Exception:
raise YunohostAuthenticationError("unable_authenticate")
if not infos:
raise YunohostAuthenticationError("unable_authenticate")
if infos["host"] != request.get_header("host"):
raise YunohostAuthenticationError("unable_authenticate")
if not user_is_allowed_on_domain(infos["user"], infos["host"]):
raise YunohostAuthenticationError("unable_authenticate")
self.purge_expired_session_files()
session_file = Path(SESSION_FOLDER) / infos["id"]
if not session_file.exists():
response.delete_cookie("yunohost.portal", path="/")
raise YunohostAuthenticationError("session_expired")
# Otherwise, we 'touch' the file to extend the validity
session_file.touch()
if decrypt_pwd:
infos["pwd"] = decrypt(infos["pwd"])
return infos
def delete_session_cookie(self):
from bottle import response
try:
infos = self.get_session_cookie()
session_file = Path(SESSION_FOLDER) / infos["id"]
session_file.unlink()
except Exception as e:
logger.debug(f"User logged out, but failed to properly invalidate the session : {e}")
response.delete_cookie("yunohost.portal", path="/")
def purge_expired_session_files(self):
for session_file in Path(SESSION_FOLDER).iterdir():
print(session_file.stat().st_mtime - time.time())
if abs(session_file.stat().st_mtime - time.time()) > SESSION_VALIDITY:
try:
session_file.unlink()
except Exception as e:
logger.debug(f"Failed to delete session file {session_file} ? {e}")
@staticmethod
def invalidate_all_sessions_for_user(user):
for file in Path(SESSION_FOLDER).glob(f"{short_hash(user)}*"):
try:
file.unlink()
except Exception as e:
logger.debug(f"Failed to delete session file {file} ? {e}")

View file

@ -30,10 +30,10 @@ from glob import glob
from collections import OrderedDict
from functools import reduce
from packaging import version
from logging import getLogger
from moulinette import Moulinette, m18n
from moulinette.utils.text import random_ascii
from moulinette.utils.log import getActionLogger
from moulinette.utils.filesystem import (
read_file,
mkdir,
@ -76,6 +76,7 @@ from yunohost.utils.system import (
binary_to_human,
space_used_by_directory,
)
from yunohost.settings import settings_get
BACKUP_PATH = "/home/yunohost.backup"
ARCHIVES_PATH = f"{BACKUP_PATH}/archives"
@ -83,7 +84,7 @@ APP_MARGIN_SPACE_SIZE = 100 # In MB
CONF_MARGIN_SPACE_SIZE = 10 # IN MB
POSTINSTALL_ESTIMATE_SPACE_SIZE = 5 # In MB
MB_ALLOWED_TO_ORGANIZE = 10
logger = getLogger("yunohost.backup")
logger = getActionLogger("yunohost.backup")
class BackupRestoreTargetsManager:
@ -1185,6 +1186,9 @@ class RestoreManager:
try:
self._postinstall_if_needed()
# Apply dirty patch to redirect php5 file on php7
self._patch_legacy_php_versions_in_csv_file()
self._restore_system()
self._restore_apps()
except Exception as e:
@ -1195,6 +1199,39 @@ class RestoreManager:
finally:
self.clean()
def _patch_legacy_php_versions_in_csv_file(self):
"""
Apply dirty patch to redirect php5 and php7.0 files to php7.4
"""
from yunohost.utils.legacy import LEGACY_PHP_VERSION_REPLACEMENTS
backup_csv = os.path.join(self.work_dir, "backup.csv")
if not os.path.isfile(backup_csv):
return
replaced_something = False
with open(backup_csv) as csvfile:
reader = csv.DictReader(csvfile, fieldnames=["source", "dest"])
newlines = []
for row in reader:
for pattern, replace in LEGACY_PHP_VERSION_REPLACEMENTS:
if pattern in row["source"]:
replaced_something = True
row["source"] = row["source"].replace(pattern, replace)
newlines.append(row)
if not replaced_something:
return
with open(backup_csv, "w") as csvfile:
writer = csv.DictWriter(
csvfile, fieldnames=["source", "dest"], quoting=csv.QUOTE_ALL
)
for row in newlines:
writer.writerow(row)
def _restore_system(self):
"""Restore user and system parts"""
@ -1331,6 +1368,8 @@ class RestoreManager:
name should be already install)
"""
from yunohost.utils.legacy import (
_patch_legacy_php_versions,
_patch_legacy_php_versions_in_settings,
_patch_legacy_helpers,
)
from yunohost.user import user_group_list
@ -1369,6 +1408,10 @@ class RestoreManager:
# Attempt to patch legacy helpers...
_patch_legacy_helpers(app_settings_in_archive)
# Apply dirty patch to make php5 apps compatible with php7
_patch_legacy_php_versions(app_settings_in_archive)
_patch_legacy_php_versions_in_settings(app_settings_in_archive)
# Delete _common.sh file in backup
common_file = os.path.join(app_backup_in_archive, "_common.sh")
rm(common_file, force=True)
@ -1880,8 +1923,6 @@ class TarBackupMethod(BackupMethod):
@property
def _archive_file(self):
from yunohost.settings import settings_get
if isinstance(self.manager, RestoreManager):
return self.manager.archive_path
@ -2513,6 +2554,9 @@ def backup_info(name, with_details=False, human_readable=False):
for category in ["apps", "system"]:
for name, key_info in info[category].items():
if category == "system":
# Stupid legacy fix for weird format between 3.5 and 3.6
if isinstance(key_info, dict):
key_info = key_info.keys()
info[category][name] = key_info = {"paths": key_info}
else:
info[category][name] = key_info

View file

@ -21,10 +21,11 @@ import sys
import shutil
import subprocess
from glob import glob
from logging import getLogger
from datetime import datetime
from moulinette import m18n
from moulinette.utils.log import getActionLogger
from moulinette.utils.filesystem import read_file, chown, chmod
from moulinette.utils.process import check_output
@ -37,7 +38,7 @@ from yunohost.service import _run_service_command
from yunohost.regenconf import regen_conf
from yunohost.log import OperationLogger
logger = getLogger("yunohost.certmanager")
logger = getActionLogger("yunohost.certmanager")
CERT_FOLDER = "/etc/yunohost/certs/"
TMP_FOLDER = "/var/www/.well-known/acme-challenge-private/"
@ -556,7 +557,6 @@ def _fetch_and_enable_new_certificate(domain, no_checks=False):
def _prepare_certificate_signing_request(domain, key_file, output_folder):
from OpenSSL import crypto # lazy loading this module for performance reasons
from yunohost.hook import hook_callback
# Init a request
csr = crypto.X509Req()
@ -564,23 +564,42 @@ def _prepare_certificate_signing_request(domain, key_file, output_folder):
# Set the domain
csr.get_subject().CN = domain
from yunohost.domain import domain_config_get
# If XMPP is enabled for this domain, add xmpp-upload and muc subdomains
# in subject alternate names
if domain_config_get(domain, key="feature.xmpp.xmpp") == 1:
subdomain = "xmpp-upload." + domain
xmpp_records = (
Diagnoser.get_cached_report(
"dnsrecords", item={"domain": domain, "category": "xmpp"}
).get("data")
or {}
)
sanlist = []
hook_results = hook_callback("cert_alternate_names", env={"domain": domain})
for hook_name, results in hook_results.items():
#
# There can be multiple results per hook name, so results look like
# {'/some/path/to/hook1':
# { 'state': 'succeed',
# 'stdreturn': ["foo", "bar"]
# },
# '/some/path/to/hook2':
# { ... },
# [...]
#
# Loop over the sub-results
for result in results.values():
if result.get("stdreturn"):
sanlist += result["stdreturn"]
# Handle the boring case where the domain is not the root of the dns zone etc...
from yunohost.dns import (
_get_relative_name_for_dns_zone,
_get_dns_zone_for_domain,
)
base_dns_zone = _get_dns_zone_for_domain(domain)
basename = _get_relative_name_for_dns_zone(domain, base_dns_zone)
suffix = f".{basename}" if basename != "@" else ""
for sub in ("xmpp-upload", "muc"):
subdomain = sub + "." + domain
if xmpp_records.get("CNAME:" + sub + suffix) == "OK":
sanlist.append(("DNS:" + subdomain))
else:
logger.warning(
m18n.n(
"certmanager_warning_subdomain_dns_record",
subdomain=subdomain,
domain=domain,
)
)
if sanlist:
csr.add_extensions(
@ -588,7 +607,7 @@ def _prepare_certificate_signing_request(domain, key_file, output_folder):
crypto.X509Extension(
b"subjectAltName",
False,
(", ".join([f"DNS:{sub}.{domain}" for sub in sanlist])).encode("utf-8"),
(", ".join(sanlist)).encode("utf-8"),
)
]
)
@ -725,6 +744,15 @@ def _enable_certificate(domain, new_cert_folder):
logger.debug("Restarting services...")
for service in ("dovecot", "metronome"):
# Ugly trick to not restart metronome if it's not installed or no domain configured for XMPP
if service == "metronome" and (
os.system("dpkg --list | grep -q 'ii *metronome'") != 0
or not glob("/etc/metronome/conf.d/*.cfg.lua")
):
continue
_run_service_command("restart", service)
if os.path.isfile("/etc/yunohost/installed"):
# regen nginx conf to be sure it integrates OCSP Stapling
# (We don't do this yet if postinstall is not finished yet)
@ -732,7 +760,6 @@ def _enable_certificate(domain, new_cert_folder):
regen_conf(names=["nginx", "postfix"])
_run_service_command("reload", "nginx")
_run_service_command("restart", "dovecot")
from yunohost.hook import hook_callback

View file

@ -19,9 +19,9 @@
import os
import json
import subprocess
import logging
from typing import List
from moulinette.utils import log
from moulinette.utils.process import check_output
from moulinette.utils.filesystem import read_file, read_json, write_to_json
from yunohost.diagnosis import Diagnoser
@ -31,7 +31,7 @@ from yunohost.utils.system import (
system_arch,
)
logger = logging.getLogger("yunohost.diagnosis")
logger = log.getActionLogger("yunohost.diagnosis")
class MyDiagnoser(Diagnoser):

View file

@ -19,9 +19,9 @@
import re
import os
import random
import logging
from typing import List
from moulinette.utils import log
from moulinette.utils.network import download_text
from moulinette.utils.process import check_output
from moulinette.utils.filesystem import read_file
@ -30,7 +30,7 @@ from yunohost.diagnosis import Diagnoser
from yunohost.utils.network import get_network_interfaces
from yunohost.settings import settings_get
logger = logging.getLogger("yunohost.diagnosis")
logger = log.getActionLogger("yunohost.diagnosis")
class MyDiagnoser(Diagnoser):

View file

@ -18,11 +18,11 @@
#
import os
import re
import logging
from typing import List
from datetime import datetime, timedelta
from publicsuffix2 import PublicSuffixList
from moulinette.utils import log
from moulinette.utils.process import check_output
from yunohost.utils.dns import (
@ -39,7 +39,7 @@ from yunohost.dns import (
_get_relative_name_for_dns_zone,
)
logger = logging.getLogger("yunohost.diagnosis")
logger = log.getActionLogger("yunohost.diagnosis")
class MyDiagnoser(Diagnoser):
@ -91,7 +91,7 @@ class MyDiagnoser(Diagnoser):
domain, include_empty_AAAA_if_no_ipv6=True
)
categories = ["basic", "mail", "extra"]
categories = ["basic", "mail", "xmpp", "extra"]
for category in categories:
records = expected_configuration[category]

View file

@ -19,11 +19,11 @@
import os
import dns.resolver
import re
import logging
from typing import List
from subprocess import CalledProcessError
from moulinette.utils import log
from moulinette.utils.process import check_output
from moulinette.utils.filesystem import read_yaml
@ -34,7 +34,7 @@ from yunohost.utils.dns import dig
DEFAULT_DNS_BLACKLIST = "/usr/share/yunohost/dnsbl_list.yml"
logger = logging.getLogger("yunohost.diagnosis")
logger = log.getActionLogger("yunohost.diagnosis")
class MyDiagnoser(Diagnoser):

View file

@ -21,9 +21,9 @@ import os
import time
import glob
from importlib import import_module
from logging import getLogger
from moulinette import m18n, Moulinette
from moulinette.utils import log
from moulinette.utils.filesystem import (
read_json,
write_to_json,
@ -33,7 +33,7 @@ from moulinette.utils.filesystem import (
from yunohost.utils.error import YunohostError, YunohostValidationError
logger = getLogger("yunohost.diagnosis")
logger = log.getActionLogger("yunohost.diagnosis")
DIAGNOSIS_CACHE = "/var/cache/yunohost/diagnosis/"
DIAGNOSIS_CONFIG_FILE = "/etc/yunohost/diagnosis.yml"

View file

@ -19,11 +19,12 @@
import os
import re
import time
from logging import getLogger
from difflib import SequenceMatcher
from collections import OrderedDict
from moulinette import m18n, Moulinette
from moulinette.utils.log import getActionLogger
from moulinette.utils.filesystem import read_file, write_to_file, read_toml, mkdir
from yunohost.domain import (
@ -37,10 +38,11 @@ from yunohost.domain import (
from yunohost.utils.dns import dig, is_yunohost_dyndns_domain, is_special_use_tld
from yunohost.utils.error import YunohostValidationError, YunohostError
from yunohost.utils.network import get_public_ip
from yunohost.settings import settings_get
from yunohost.log import is_unit_operation
from yunohost.hook import hook_callback
logger = getLogger("yunohost.domain")
logger = getActionLogger("yunohost.domain")
DOMAIN_REGISTRAR_LIST_PATH = "/usr/share/yunohost/registrar_list.toml"
@ -75,6 +77,12 @@ def domain_dns_suggest(domain):
result += "\n{name} {ttl} IN {type} {value}".format(**record)
result += "\n\n"
if dns_conf["xmpp"]:
result += "\n\n"
result += "; XMPP"
for record in dns_conf["xmpp"]:
result += "\n{name} {ttl} IN {type} {value}".format(**record)
if dns_conf["extra"]:
result += "\n\n"
result += "; Extra"
@ -82,7 +90,7 @@ def domain_dns_suggest(domain):
result += "\n{name} {ttl} IN {type} {value}".format(**record)
for name, record_list in dns_conf.items():
if name not in ("basic", "mail", "extra") and record_list:
if name not in ("basic", "xmpp", "mail", "extra") and record_list:
result += "\n\n"
result += "; " + name
for record in record_list:
@ -111,6 +119,14 @@ def _build_dns_conf(base_domain, include_empty_AAAA_if_no_ipv6=False):
# if ipv6 available
{"type": "AAAA", "name": "@", "value": "valid-ipv6", "ttl": 3600},
],
"xmpp": [
{"type": "SRV", "name": "_xmpp-client._tcp", "value": "0 5 5222 domain.tld.", "ttl": 3600},
{"type": "SRV", "name": "_xmpp-server._tcp", "value": "0 5 5269 domain.tld.", "ttl": 3600},
{"type": "CNAME", "name": "muc", "value": "@", "ttl": 3600},
{"type": "CNAME", "name": "pubsub", "value": "@", "ttl": 3600},
{"type": "CNAME", "name": "vjud", "value": "@", "ttl": 3600}
{"type": "CNAME", "name": "xmpp-upload", "value": "@", "ttl": 3600}
],
"mail": [
{"type": "MX", "name": "@", "value": "10 domain.tld.", "ttl": 3600},
{"type": "TXT", "name": "@", "value": "\"v=spf1 a mx ip4:123.123.123.123 ipv6:valid-ipv6 -all\"", "ttl": 3600 },
@ -130,10 +146,9 @@ def _build_dns_conf(base_domain, include_empty_AAAA_if_no_ipv6=False):
}
"""
from yunohost.settings import settings_get
basic = []
mail = []
xmpp = []
extra = []
ipv4 = get_public_ip()
ipv6 = get_public_ip(6)
@ -196,6 +211,29 @@ def _build_dns_conf(base_domain, include_empty_AAAA_if_no_ipv6=False):
[f"_dmarc{suffix}", ttl, "TXT", '"v=DMARC1; p=none"'],
]
########
# XMPP #
########
if settings["xmpp"]:
xmpp += [
[
f"_xmpp-client._tcp{suffix}",
ttl,
"SRV",
f"0 5 5222 {domain}.",
],
[
f"_xmpp-server._tcp{suffix}",
ttl,
"SRV",
f"0 5 5269 {domain}.",
],
[f"muc{suffix}", ttl, "CNAME", f"{domain}."],
[f"pubsub{suffix}", ttl, "CNAME", f"{domain}."],
[f"vjud{suffix}", ttl, "CNAME", f"{domain}."],
[f"xmpp-upload{suffix}", ttl, "CNAME", f"{domain}."],
]
#########
# Extra #
#########
@ -221,6 +259,10 @@ def _build_dns_conf(base_domain, include_empty_AAAA_if_no_ipv6=False):
{"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
@ -235,8 +277,15 @@ def _build_dns_conf(base_domain, include_empty_AAAA_if_no_ipv6=False):
# Custom records #
##################
# Defined by custom hooks shipped in apps for example ...
hook_results = hook_callback("custom_dns_rules", env={"base_domain": base_domain, "suffix": suffix})
# Defined by custom hooks ships in apps for example ...
# FIXME : this ain't practical for apps that may want to add
# custom dns records for a subdomain ... there's no easy way for
# an app to compare the base domain is the parent of the subdomain ?
# (On the other hand, in sep 2021, it looks like no app is using
# this mechanism...)
hook_results = hook_callback("custom_dns_rules", args=[base_domain])
for hook_name, results in hook_results.items():
#
# There can be multiple results per hook name, so results look like
@ -463,26 +512,11 @@ def _get_relative_name_for_dns_zone(domain, base_dns_zone):
def _get_registrar_config_section(domain):
from lexicon.providers.auto import _relevant_provider_for_domain
registrar_infos = OrderedDict(
{
registrar_infos = {
"name": m18n.n(
"registrar_infos"
), # This is meant to name the config panel section, for proper display in the webadmin
"registrar": OrderedDict(
{
"readonly": True,
"visible": False,
"default": None,
}
),
"infos": OrderedDict(
{
"type": "alert",
"style": "info",
}
),
}
)
dns_zone = _get_dns_zone_for_domain(domain)
@ -495,20 +529,31 @@ def _get_registrar_config_section(domain):
else:
parent_domain_link = parent_domain
registrar_infos["registrar"]["default"] = "parent_domain"
registrar_infos["infos"]["ask"] = m18n.n(
registrar_infos["registrar"] = OrderedDict(
{
"type": "alert",
"style": "info",
"ask": m18n.n(
"domain_dns_registrar_managed_in_parent_domain",
parent_domain=parent_domain,
parent_domain_link=parent_domain_link,
),
"value": "parent_domain",
}
)
return registrar_infos
return OrderedDict(registrar_infos)
# TODO big project, integrate yunohost's dynette as a registrar-like provider
# TODO big project, integrate other dyndns providers such as netlib.re, or cf the list of dyndns providers supported by cloudron...
if is_yunohost_dyndns_domain(dns_zone):
registrar_infos["registrar"]["default"] = "yunohost"
registrar_infos["infos"]["style"] = "success"
registrar_infos["infos"]["ask"] = m18n.n("domain_dns_registrar_yunohost")
registrar_infos["registrar"] = OrderedDict(
{
"type": "alert",
"style": "success",
"ask": m18n.n("domain_dns_registrar_yunohost"),
"value": "yunohost",
}
)
registrar_infos["recovery_password"] = OrderedDict(
{
"type": "password",
@ -516,22 +561,36 @@ def _get_registrar_config_section(domain):
"default": "",
}
)
return registrar_infos
return OrderedDict(registrar_infos)
elif is_special_use_tld(dns_zone):
registrar_infos["infos"]["ask"] = m18n.n("domain_dns_conf_special_use_tld")
registrar_infos["registrar"] = OrderedDict(
{
"type": "alert",
"style": "info",
"ask": m18n.n("domain_dns_conf_special_use_tld"),
"value": None,
}
)
try:
registrar = _relevant_provider_for_domain(dns_zone)[0]
except ValueError:
registrar_infos["registrar"]["default"] = None
registrar_infos["infos"]["ask"] = m18n.n("domain_dns_registrar_not_supported")
registrar_infos["infos"]["style"] = "warning"
registrar_infos["registrar"] = OrderedDict(
{
"type": "alert",
"style": "warning",
"ask": m18n.n("domain_dns_registrar_not_supported"),
"value": None,
}
)
else:
registrar_infos["registrar"]["default"] = registrar
registrar_infos["infos"]["ask"] = m18n.n(
"domain_dns_registrar_supported", registrar=registrar
registrar_infos["registrar"] = OrderedDict(
{
"type": "alert",
"style": "info",
"ask": m18n.n("domain_dns_registrar_supported", registrar=registrar),
"value": registrar,
}
)
TESTED_REGISTRARS = ["ovh", "gandi"]
@ -559,7 +618,7 @@ def _get_registrar_config_section(domain):
infos["optional"] = infos.get("optional", "False")
registrar_infos.update(registrar_credentials)
return registrar_infos
return OrderedDict(registrar_infos)
def _get_registar_settings(domain):

View file

@ -18,35 +18,28 @@
#
import os
import time
from pathlib import Path
from typing import TYPE_CHECKING, Any, List, Optional, Union
from typing import List, Optional
from collections import OrderedDict
from logging import getLogger
from moulinette import m18n, Moulinette
from moulinette.core import MoulinetteError
from moulinette.utils.filesystem import (
read_json,
read_yaml,
rm,
read_file,
write_to_file,
write_to_json,
write_to_yaml,
)
from moulinette.utils.log import getActionLogger
from moulinette.utils.filesystem import write_to_file, read_yaml, write_to_yaml, rm
from yunohost.app import (
app_ssowatconf,
_installed_apps,
_get_app_settings,
_get_conflicting_apps,
)
from yunohost.regenconf import regen_conf, _force_clear_hashes, _process_regen_conf
from yunohost.utils.configpanel import ConfigPanel
from yunohost.utils.form import BaseOption
from yunohost.utils.error import YunohostError, YunohostValidationError
from yunohost.utils.dns import is_yunohost_dyndns_domain
from yunohost.log import is_unit_operation
if TYPE_CHECKING:
from pydantic.typing import AbstractSetIntStr, MappingIntStrAny
from yunohost.utils.configpanel import RawConfig
from yunohost.utils.form import FormModel
from yunohost.utils.configpanel import RawSettings
logger = getLogger("yunohost.domain")
logger = getActionLogger("yunohost.domain")
DOMAIN_SETTINGS_DIR = "/etc/yunohost/domains"
@ -107,30 +100,6 @@ def _get_domains(exclude_subdomains=False):
return domain_list_cache
def _get_domain_portal_dict():
domains = _get_domains()
out = OrderedDict()
for domain in domains:
parent = None
# Use the topest parent domain if any
for d in out.keys():
if domain.endswith(f".{d}"):
parent = d
break
out[domain] = f'{parent or domain}/yunohost/sso'
# By default, redirect to $host/yunohost/admin for domains not listed in the dict
# maybe in the future, we can allow to tweak this
out["default"] = "/yunohost/admin"
return dict(out)
def domain_list(exclude_subdomains=False, tree=False, features=[]):
"""
List domains
@ -184,14 +153,13 @@ def domain_info(domain):
domain -- Domain to be checked
"""
from yunohost.app import app_info, _installed_apps, _get_app_settings
from yunohost.app import app_info
from yunohost.dns import _get_registar_settings
from yunohost.certificate import certificate_status
_assert_domain_exists(domain)
registrar, _ = _get_registar_settings(domain)
certificate = certificate_status([domain], full=True)["certificates"][domain]
certificate = domain_cert_status([domain], full=True)["certificates"][domain]
apps = []
for app in _installed_apps():
@ -261,11 +229,16 @@ def domain_add(
from yunohost.utils.ldap import _get_ldap_interface
from yunohost.utils.password import assert_password_is_strong_enough
from yunohost.certificate import _certificate_install_selfsigned
from yunohost.utils.dns import is_yunohost_dyndns_domain
if dyndns_recovery_password:
operation_logger.data_to_redact.append(dyndns_recovery_password)
if domain.startswith("xmpp-upload."):
raise YunohostValidationError("domain_cannot_add_xmpp_upload")
if domain.startswith("muc."):
raise YunohostError("domain_cannot_add_muc_upload")
ldap = _get_ldap_interface()
try:
@ -334,8 +307,10 @@ def domain_add(
regen_conf(
names=[
"nginx",
"metronome",
"dnsmasq",
"postfix",
"rspamd",
"mdns",
"dovecot",
]
@ -377,15 +352,8 @@ def domain_remove(
"""
import glob
from yunohost.hook import hook_callback
from yunohost.app import (
app_ssowatconf,
app_info,
app_remove,
_get_app_settings,
_installed_apps,
)
from yunohost.app import app_ssowatconf, app_info, app_remove
from yunohost.utils.ldap import _get_ldap_interface
from yunohost.utils.dns import is_yunohost_dyndns_domain
if dyndns_recovery_password:
operation_logger.data_to_redact.append(dyndns_recovery_password)
@ -506,7 +474,7 @@ def domain_remove(
f"/etc/nginx/conf.d/{domain}.conf", new_conf=None, save=True
)
regen_conf(names=["nginx", "dnsmasq", "postfix", "mdns"])
regen_conf(names=["nginx", "metronome", "dnsmasq", "postfix", "rspamd", "mdns"])
app_ssowatconf()
hook_callback("post_domain_remove", args=[domain])
@ -592,6 +560,9 @@ def domain_main_domain(operation_logger, new_main_domain=None):
logger.warning(str(e), exc_info=1)
raise YunohostError("main_domain_change_failed")
# Generate SSOwat configuration file
app_ssowatconf()
# Regen configurations
if os.path.exists("/etc/yunohost/installed"):
regen_conf()
@ -614,25 +585,9 @@ def domain_url_available(domain, path):
path -- The path to check (e.g. /coffee)
"""
from yunohost.app import _get_conflicting_apps
return len(_get_conflicting_apps(domain, path)) == 0
def _get_raw_domain_settings(domain):
"""Get domain settings directly from file.
Be carefull, domain settings are saved in `"diff"` mode (i.e. default settings are not saved)
so the file may be completely empty
"""
_assert_domain_exists(domain)
# NB: this corresponds to save_path_tpl in DomainConfigPanel
path = f"{DOMAIN_SETTINGS_DIR}/{domain}.yml"
if os.path.exists(path):
return read_yaml(path)
return {}
def domain_config_get(domain, key="", full=False, export=False):
"""
Display a domain configuration
@ -650,7 +605,6 @@ def domain_config_get(domain, key="", full=False, export=False):
else:
mode = "classic"
DomainConfigPanel = _get_DomainConfigPanel()
config = DomainConfigPanel(domain)
return config.get(key, mode)
@ -662,198 +616,161 @@ def domain_config_set(
"""
Apply a new domain configuration
"""
from yunohost.utils.form import BaseOption
DomainConfigPanel = _get_DomainConfigPanel()
BaseOption.operation_logger = operation_logger
config = DomainConfigPanel(domain)
return config.set(key, value, args, args_file, operation_logger=operation_logger)
def _get_DomainConfigPanel():
from yunohost.utils.configpanel import ConfigPanel
class DomainConfigPanel(ConfigPanel):
entity_type = "domain"
save_path_tpl = f"{DOMAIN_SETTINGS_DIR}/{{entity}}.yml"
save_mode = "diff"
def get(self, key="", mode="classic"):
result = super().get(key=key, mode=mode)
if mode == "full":
for panel, section, option in self._iterate():
# This injects:
# i18n: domain_config_cert_renew_help
# i18n: domain_config_default_app_help
# i18n: domain_config_xmpp_help
if m18n.key_exists(self.config["i18n"] + "_" + option["id"] + "_help"):
option["help"] = m18n.n(
self.config["i18n"] + "_" + option["id"] + "_help"
)
return self.config
def _get_raw_config(self) -> "RawConfig":
# TODO add mechanism to share some settings with other domains on the same zone
raw_config = super()._get_raw_config()
return result
any_filter = all(self.filter_key)
panel_id, section_id, option_id = self.filter_key
def _get_raw_config(self):
toml = super()._get_raw_config()
# Portal settings are only available on "topest" domains
if _get_parent_domain_of(self.entity, topest=True) is not None:
del raw_config["feature"]["portal"]
toml["feature"]["xmpp"]["xmpp"]["default"] = (
1 if self.entity == _get_maindomain() else 0
)
# Optimize wether or not to load the DNS section,
# e.g. we don't want to trigger the whole _get_registary_config_section
# when just getting the current value from the feature section
if not any_filter or panel_id == "dns":
filter_key = self.filter_key.split(".") if self.filter_key != "" else []
if not filter_key or filter_key[0] == "dns":
from yunohost.dns import _get_registrar_config_section
raw_config["dns"]["registrar"] = _get_registrar_config_section(
self.entity
)
toml["dns"]["registrar"] = _get_registrar_config_section(self.entity)
# FIXME: Ugly hack to save the registar id/value and reinject it in _get_raw_settings ...
self.registar_id = toml["dns"]["registrar"]["registrar"]["value"]
del toml["dns"]["registrar"]["registrar"]["value"]
# Cert stuff
if not any_filter or panel_id == "cert":
if not filter_key or filter_key[0] == "cert":
from yunohost.certificate import certificate_status
status = certificate_status([self.entity], full=True)["certificates"][
self.entity
]
raw_config["cert"]["cert_"]["cert_summary"]["style"] = status["style"]
toml["cert"]["cert"]["cert_summary"]["style"] = status["style"]
# i18n: domain_config_cert_summary_expired
# i18n: domain_config_cert_summary_selfsigned
# i18n: domain_config_cert_summary_abouttoexpire
# i18n: domain_config_cert_summary_ok
# i18n: domain_config_cert_summary_letsencrypt
raw_config["cert"]["cert_"]["cert_summary"]["ask"] = m18n.n(
toml["cert"]["cert"]["cert_summary"]["ask"] = m18n.n(
f"domain_config_cert_summary_{status['summary']}"
)
for option_id, status_key in [
("cert_validity", "validity"),
("cert_issuer", "CA_type"),
("acme_eligible", "ACME_eligible"),
# FIXME not sure why "summary" was injected in settings values
# ("summary", "summary")
]:
raw_config["cert"]["cert_"][option_id]["default"] = status[
status_key
]
# FIXME: Ugly hack to save the cert status and reinject it in _get_raw_settings ...
self.cert_status = status
# Other specific strings used in config panels
# i18n: domain_config_cert_renew_help
return toml
return raw_config
def _get_raw_settings(self):
# TODO add mechanism to share some settings with other domains on the same zone
super()._get_raw_settings()
def _get_raw_settings(self) -> "RawSettings":
raw_settings = super()._get_raw_settings()
# FIXME: Ugly hack to save the registar id/value and reinject it in _get_raw_settings ...
filter_key = self.filter_key.split(".") if self.filter_key != "" else []
if not filter_key or filter_key[0] == "dns":
self.values["registrar"] = self.registar_id
custom_css = Path(f"/usr/share/yunohost/portal/customassets/{self.entity}.custom.css")
if custom_css.exists():
raw_settings["custom_css"] = read_file(str(custom_css))
# FIXME: Ugly hack to save the cert status and reinject it in _get_raw_settings ...
if not filter_key or filter_key[0] == "cert":
self.values["cert_validity"] = self.cert_status["validity"]
self.values["cert_issuer"] = self.cert_status["CA_type"]
self.values["acme_eligible"] = self.cert_status["ACME_eligible"]
self.values["summary"] = self.cert_status["summary"]
return raw_settings
def _apply(
self,
form: "FormModel",
previous_settings: dict[str, Any],
exclude: Union["AbstractSetIntStr", "MappingIntStrAny", None] = None,
) -> None:
next_settings = {
k: v for k, v in form.dict().items() if previous_settings.get(k) != v
}
if "default_app" in next_settings:
from yunohost.app import app_map
def _apply(self):
if (
"default_app" in self.future_values
and self.future_values["default_app"] != self.values["default_app"]
):
from yunohost.app import app_ssowatconf, app_map
if "/" in app_map(raw=True).get(self.entity, {}):
raise YunohostValidationError(
"app_make_default_location_already_used",
app=next_settings["default_app"],
app=self.future_values["default_app"],
domain=self.entity,
other_app=app_map(raw=True)[self.entity]["/"]["id"],
)
if next_settings.get("recovery_password", None):
domain_dyndns_set_recovery_password(
self.entity, next_settings["recovery_password"]
)
custom_css = next_settings.pop("custom_css", "").strip()
if custom_css:
write_to_file(f"/usr/share/yunohost/portal/customassets/{self.entity}.custom.css", custom_css)
# Make sure the value doesnt get written in the yml
form.custom_css = ""
portal_options = [
"enable_public_apps_page",
"show_other_domains_apps",
"portal_title",
"portal_logo",
"portal_theme",
"search_engine",
"search_engine_name",
"portal_user_intro",
"portal_public_intro",
]
if _get_parent_domain_of(self.entity, topest=True) is None and any(
option in next_settings for option in portal_options
if (
"recovery_password" in self.new_values
and self.new_values["recovery_password"]
):
from yunohost.portal import PORTAL_SETTINGS_DIR
# Portal options are also saved in a `domain.portal.yml` file
# that can be read by the portal API.
# FIXME remove those from the config panel saved values?
portal_values = form.dict(include=set(portal_options))
# Remove logo from values else filename will replace b64 content
if "portal_logo" in portal_values:
portal_values.pop("portal_logo")
if "portal_logo" in next_settings:
if previous_settings.get("portal_logo"):
try:
os.remove(previous_settings["portal_logo"])
except FileNotFoundError:
logger.warning(
f"Coulnd't remove previous logo file, maybe the file was already deleted, path: {previous_settings['portal_logo']}"
domain_dyndns_set_recovery_password(
self.entity, self.new_values["recovery_password"]
)
finally:
portal_values["portal_logo"] = ""
# Do not save password in yaml settings
if "recovery_password" in self.values:
del self.values["recovery_password"]
if "recovery_password" in self.new_values:
del self.new_values["recovery_password"]
assert "recovery_password" not in self.future_values
if next_settings["portal_logo"]:
portal_values["portal_logo"] = Path(next_settings["portal_logo"]).name
portal_settings_path = Path(f"{PORTAL_SETTINGS_DIR}/{self.entity}.json")
portal_settings: dict[str, Any] = {"apps": {}}
if portal_settings_path.exists():
portal_settings.update(read_json(str(portal_settings_path)))
# Merge settings since this config file is shared with `app_ssowatconf()` which populate the `apps` key.
portal_settings.update(portal_values)
write_to_json(
str(portal_settings_path), portal_settings, sort_keys=True, indent=4
)
super()._apply(form, previous_settings, exclude={"recovery_password"})
super()._apply()
# Reload ssowat if default app changed
if "default_app" in next_settings or "enable_public_apps_page" in next_settings:
from yunohost.app import app_ssowatconf
if (
"default_app" in self.future_values
and self.future_values["default_app"] != self.values["default_app"]
):
app_ssowatconf()
stuff_to_regen_conf = set()
if "mail_in" in next_settings or "mail_out" in next_settings:
stuff_to_regen_conf.update({"nginx", "postfix", "dovecot"})
stuff_to_regen_conf = []
if (
"xmpp" in self.future_values
and self.future_values["xmpp"] != self.values["xmpp"]
):
stuff_to_regen_conf.append("nginx")
stuff_to_regen_conf.append("metronome")
if (
"mail_in" in self.future_values
and self.future_values["mail_in"] != self.values["mail_in"]
) or (
"mail_out" in self.future_values
and self.future_values["mail_out"] != self.values["mail_out"]
):
if "nginx" not in stuff_to_regen_conf:
stuff_to_regen_conf.append("nginx")
stuff_to_regen_conf.append("postfix")
stuff_to_regen_conf.append("dovecot")
stuff_to_regen_conf.append("rspamd")
if stuff_to_regen_conf:
regen_conf(names=list(stuff_to_regen_conf))
return DomainConfigPanel
regen_conf(names=stuff_to_regen_conf)
def domain_action_run(domain, action, args=None):
import urllib.parse
if action == "cert.cert_.cert_install":
if action == "cert.cert.cert_install":
from yunohost.certificate import certificate_install as action_func
elif action == "cert.cert_.cert_renew":
elif action == "cert.cert.cert_renew":
from yunohost.certificate import certificate_renew as action_func
args = dict(urllib.parse.parse_qsl(args or "", keep_blank_values=True))
@ -902,6 +819,10 @@ def domain_cert_renew(domain_list, force=False, no_checks=False, email=False):
return certificate_renew(domain_list, force, no_checks, email)
def domain_dns_conf(domain):
return domain_dns_suggest(domain)
def domain_dns_suggest(domain):
from yunohost.dns import domain_dns_suggest

View file

@ -22,10 +22,10 @@ import glob
import base64
import subprocess
import hashlib
from logging import getLogger
from moulinette import Moulinette, m18n
from moulinette.core import MoulinetteError
from moulinette.utils.log import getActionLogger
from moulinette.utils.filesystem import write_to_file, rm, chown, chmod
from yunohost.utils.error import YunohostError, YunohostValidationError
@ -35,7 +35,7 @@ from yunohost.utils.dns import dig, is_yunohost_dyndns_domain
from yunohost.log import is_unit_operation
from yunohost.regenconf import regen_conf
logger = getLogger("yunohost.dyndns")
logger = getActionLogger("yunohost.dyndns")
DYNDNS_PROVIDER = "dyndns.yunohost.org"
DYNDNS_DNS_AUTH = ["ns0.yunohost.org", "ns1.yunohost.org"]
@ -471,7 +471,7 @@ def dyndns_update(
# Delete custom DNS records, we don't support them (have to explicitly
# authorize them on dynette)
for category in dns_conf.keys():
if category not in ["basic", "mail", "extra"]:
if category not in ["basic", "mail", "xmpp", "extra"]:
del dns_conf[category]
# Delete the old records for all domain/subdomains

View file

@ -19,16 +19,16 @@
import os
import yaml
import miniupnpc
from logging import getLogger
from moulinette import m18n
from yunohost.utils.error import YunohostError, YunohostValidationError
from moulinette.utils import process
from moulinette.utils.log import getActionLogger
FIREWALL_FILE = "/etc/yunohost/firewall.yml"
UPNP_CRON_JOB = "/etc/cron.d/yunohost-firewall-upnp"
logger = getLogger("yunohost.firewall")
logger = getActionLogger("yunohost.firewall")
def firewall_allow(
@ -402,13 +402,7 @@ def firewall_upnp(action="status", no_refresh=False):
# Discover UPnP device(s)
logger.debug("discovering UPnP devices...")
try:
nb_dev = upnpc.discover()
except Exception:
logger.warning("Failed to find any UPnP device on the network")
nb_dev = -1
enabled = False
logger.debug("found %d UPnP device(s)", int(nb_dev))
if nb_dev < 1:
logger.error(m18n.n("upnp_dev_not_found"))

View file

@ -23,16 +23,16 @@ import tempfile
import mimetypes
from glob import iglob
from importlib import import_module
from logging import getLogger
from moulinette import m18n, Moulinette
from yunohost.utils.error import YunohostError, YunohostValidationError
from moulinette.utils import log
from moulinette.utils.filesystem import read_yaml, cp
HOOK_FOLDER = "/usr/share/yunohost/hooks/"
CUSTOM_HOOK_FOLDER = "/etc/yunohost/hooks.d/"
logger = getLogger("yunohost.hook")
logger = log.getActionLogger("yunohost.hook")
def hook_add(app, file):
@ -359,7 +359,6 @@ def hook_exec(
r"Removing obsolete dictionary files",
r"Creating new PostgreSQL cluster",
r"/usr/lib/postgresql/13/bin/initdb",
r"/usr/lib/postgresql/15/bin/initdb",
r"The files belonging to this database system will be owned by user",
r"This user must also own the server process.",
r"The database cluster will be initialized with locale",
@ -367,7 +366,6 @@ def hook_exec(
r"The default text search configuration will be set to",
r"Data page checksums are disabled.",
r"fixing permissions on existing directory /var/lib/postgresql/13/main ... ok",
r"fixing permissions on existing directory /var/lib/postgresql/15/main ... ok",
r"creating subdirectories \.\.\. ok",
r"selecting dynamic .* \.\.\. ",
r"selecting default .* \.\.\. ",

View file

@ -1,3 +1,4 @@
#
# Copyright (c) 2024 YunoHost Contributors
#
# This file is part of YunoHost (see https://yunohost.org)
@ -32,11 +33,13 @@ from moulinette import m18n, Moulinette
from moulinette.core import MoulinetteError
from yunohost.utils.error import YunohostError, YunohostValidationError
from yunohost.utils.system import get_ynh_package_version
from moulinette.utils.log import getActionLogger
from moulinette.utils.filesystem import read_file, read_yaml
logger = getLogger("yunohost.log")
logger = getActionLogger("yunohost.log")
OPERATIONS_PATH = "/var/log/yunohost/operations/"
CATEGORIES_PATH = "/var/log/yunohost/categories/"
OPERATIONS_PATH = "/var/log/yunohost/categories/operation/"
METADATA_FILE_EXT = ".yml"
LOG_FILE_EXT = ".log"
@ -285,7 +288,7 @@ def log_show(
infos = {}
# If it's a unit operation, display the name and the description
if base_path.startswith(OPERATIONS_PATH):
if base_path.startswith(CATEGORIES_PATH):
infos["description"] = _get_description_from_name(base_filename)
infos["name"] = base_filename
@ -388,17 +391,12 @@ def log_show(
def log_share(path):
return log_show(path, share=True)
from typing import TypeVar, Callable, Concatenate, ParamSpec
#FuncT = TypeVar("FuncT", bound=Callable[..., Any])
Param = ParamSpec("Param")
RetType = TypeVar("RetType")
def is_unit_operation(
entities=["app", "domain", "group", "service", "user"],
exclude=["password"],
) -> Callable[[Callable[Concatenate["OperationLogger", Param], RetType]], Callable[Param, RetType]]:
operation_key=None,
):
"""
Configure quickly a unit operation
@ -415,10 +413,17 @@ def is_unit_operation(
called 'password' are removed. If an argument is an object, you need to
exclude it or create manually the unit operation without this decorator.
operation_key A key to describe the unit operation log used to create the
filename and search a translation. Please ensure that this key prefixed by
'log_' is present in locales/en.json otherwise it won't be translatable.
"""
def decorate(func: Callable[Concatenate["OperationLogger", Param], RetType]) -> Callable[Param, RetType]:
def decorate(func):
def func_wrapper(*args, **kwargs):
op_key = operation_key
if op_key is None:
op_key = func.__name__
# If the function is called directly from an other part of the code
# and not by the moulinette framework, we need to complete kwargs
@ -469,7 +474,7 @@ def is_unit_operation(
context[field] = value.name
except Exception:
context[field] = "IOBase"
operation_logger = OperationLogger(func.__name__, related_to, args=context)
operation_logger = OperationLogger(op_key, related_to, args=context)
try:
# Start the actual function, and give the unit operation
@ -537,7 +542,7 @@ class OperationLogger:
This class record logs and metadata like context or start time/end time.
"""
_instances: List["OperationLogger"] = []
_instances: List[object] = []
def __init__(self, operation, related_to=None, **kwargs):
# TODO add a way to not save password on app installation
@ -813,7 +818,7 @@ class OperationLogger:
# 2019-10-19 16:10:27,611: DEBUG - + mysql -u piwigo --password=********** -B piwigo
# And we just want the part starting by "DEBUG - "
lines = [line for line in lines if ":" in line.strip()]
lines = [line.strip().split(": ", 1)[-1] for line in lines]
lines = [line.strip().split(": ", 1)[1] for line in lines]
# And we ignore boring/irrelevant lines
# Annnnnnd we also ignore lines matching [number] + such as
# 72971 DEBUG 29739 + ynh_exit_properly

View file

@ -0,0 +1,582 @@
import glob
import os
from moulinette import m18n
from yunohost.utils.error import YunohostError
from moulinette.utils.log import getActionLogger
from moulinette.utils.process import check_output, call_async_output
from moulinette.utils.filesystem import read_file, rm, write_to_file
from yunohost.tools import (
Migration,
tools_update,
tools_upgrade,
_apt_log_line_is_relevant,
)
from yunohost.app import unstable_apps
from yunohost.regenconf import manually_modified_files, _force_clear_hashes
from yunohost.utils.system import (
free_space_in_directory,
get_ynh_package_version,
_list_upgradable_apt_packages,
)
from yunohost.service import _get_services, _save_services
logger = getActionLogger("yunohost.migration")
N_CURRENT_DEBIAN = 10
N_CURRENT_YUNOHOST = 4
VENV_REQUIREMENTS_SUFFIX = ".requirements_backup_for_bullseye_upgrade.txt"
def _get_all_venvs(dir, level=0, maxlevel=3):
"""
Returns the list of all python virtual env directories recursively
Arguments:
dir - the directory to scan in
maxlevel - the depth of the recursion
level - do not edit this, used as an iterator
"""
if not os.path.exists(dir):
return []
result = []
# Using os functions instead of glob, because glob doesn't support hidden folders, and we need recursion with a fixed depth
for file in os.listdir(dir):
path = os.path.join(dir, file)
if os.path.isdir(path):
activatepath = os.path.join(path, "bin", "activate")
if os.path.isfile(activatepath):
content = read_file(activatepath)
if ("VIRTUAL_ENV" in content) and ("PYTHONHOME" in content):
result.append(path)
continue
if level < maxlevel:
result += _get_all_venvs(path, level=level + 1)
return result
def _backup_pip_freeze_for_python_app_venvs():
"""
Generate a requirements file for all python virtual env located inside /opt/ and /var/www/
"""
venvs = _get_all_venvs("/opt/") + _get_all_venvs("/var/www/")
for venv in venvs:
# Generate a requirements file from venv
os.system(
f"{venv}/bin/pip freeze > {venv}{VENV_REQUIREMENTS_SUFFIX} 2>/dev/null"
)
class MyMigration(Migration):
"Upgrade the system to Debian Bullseye and Yunohost 11.x"
mode = "manual"
def run(self):
self.check_assertions()
logger.info(m18n.n("migration_0021_start"))
#
# Add new apt .deb signing key
#
new_apt_key = "https://forge.yunohost.org/yunohost_bullseye.asc"
check_output(f"wget -O- {new_apt_key} -q | apt-key add -qq -")
#
# Patch sources.list
#
logger.info(m18n.n("migration_0021_patching_sources_list"))
self.patch_apt_sources_list()
# Stupid OVH has some repo configured which dont work with bullseye and break apt ...
os.system("sudo rm -f /etc/apt/sources.list.d/ovh-*.list")
# Force add sury if it's not there yet
# This is to solve some weird issue with php-common breaking php7.3-common,
# hence breaking many php7.3-deps
# hence triggering some dependency conflict (or foobar-ynh-deps uninstall)
# Adding it there shouldnt be a big deal - Yunohost 11.x does add it
# through its regen conf anyway.
if not os.path.exists("/etc/apt/sources.list.d/extra_php_version.list"):
open("/etc/apt/sources.list.d/extra_php_version.list", "w").write(
"deb https://packages.sury.org/php/ bullseye main"
)
# Add Sury key even if extra_php_version.list was already there,
# because some old system may be using an outdated key not valid for Bullseye
# and that'll block the migration
os.system(
'wget --timeout 900 --quiet "https://packages.sury.org/php/apt.gpg" --output-document=- | gpg --dearmor >"/etc/apt/trusted.gpg.d/extra_php_version.gpg"'
)
# Remove legacy, duplicated sury entry if it exists
if os.path.exists("/etc/apt/sources.list.d/sury.list"):
os.system("rm -rf /etc/apt/sources.list.d/sury.list")
#
# Get requirements of the different venvs from python apps
#
_backup_pip_freeze_for_python_app_venvs()
#
# Run apt update
#
tools_update(target="system")
# Tell libc6 it's okay to restart system stuff during the upgrade
os.system(
"echo 'libc6 libraries/restart-without-asking boolean true' | debconf-set-selections"
)
# Do not restart nginx during the upgrade of nginx-common and nginx-extras ...
# c.f. https://manpages.debian.org/bullseye/init-system-helpers/deb-systemd-invoke.1p.en.html
# and zcat /usr/share/doc/init-system-helpers/README.policy-rc.d.gz
# and the code inside /usr/bin/deb-systemd-invoke to see how it calls /usr/sbin/policy-rc.d ...
# and also invoke-rc.d ...
write_to_file(
"/usr/sbin/policy-rc.d",
'#!/bin/bash\n[[ "$1" =~ "nginx" ]] && [[ "$2" == "restart" ]] && exit 101 || exit 0',
)
os.system("chmod +x /usr/sbin/policy-rc.d")
# Don't send an email to root about the postgresql migration. It should be handled automatically after.
os.system(
"echo 'postgresql-common postgresql-common/obsolete-major seen true' | debconf-set-selections"
)
#
# Patch yunohost conflicts
#
logger.info(m18n.n("migration_0021_patch_yunohost_conflicts"))
self.patch_yunohost_conflicts()
#
# Specific tweaking to get rid of custom my.cnf and use debian's default one
# (my.cnf is actually a symlink to mariadb.cnf)
#
_force_clear_hashes(["/etc/mysql/my.cnf"])
rm("/etc/mysql/mariadb.cnf", force=True)
rm("/etc/mysql/my.cnf", force=True)
ret = self.apt_install(
"mariadb-common --reinstall -o Dpkg::Options::='--force-confmiss'"
)
if ret != 0:
raise YunohostError("Failed to reinstall mariadb-common ?", raw_msg=True)
#
# /usr/share/yunohost/yunohost-config/ssl/yunoCA -> /usr/share/yunohost/ssl
#
if os.path.exists("/usr/share/yunohost/yunohost-config/ssl/yunoCA"):
os.system(
"mv /usr/share/yunohost/yunohost-config/ssl/yunoCA /usr/share/yunohost/ssl"
)
rm("/usr/share/yunohost/yunohost-config", recursive=True, force=True)
#
# /home/yunohost.conf -> /var/cache/yunohost/regenconf
#
if os.path.exists("/home/yunohost.conf"):
os.system("mv /home/yunohost.conf /var/cache/yunohost/regenconf")
rm("/home/yunohost.conf", recursive=True, force=True)
# Remove legacy postgresql service record added by helpers,
# will now be dynamically handled by the core in bullseye
services = _get_services()
if "postgresql" in services:
del services["postgresql"]
_save_services(services)
#
# Critical fix for RPI otherwise network is down after rebooting
# https://forum.yunohost.org/t/20652
#
if os.system("systemctl | grep -q dhcpcd") == 0:
logger.info("Applying fix for DHCPCD ...")
os.system("mkdir -p /etc/systemd/system/dhcpcd.service.d")
write_to_file(
"/etc/systemd/system/dhcpcd.service.d/wait.conf",
"[Service]\nExecStart=\nExecStart=/usr/sbin/dhcpcd -w",
)
#
# Another boring fix for the super annoying libc6-dev: Breaks libgcc-8-dev
# https://forum.yunohost.org/t/20617
#
if (
os.system("dpkg --list | grep '^ii' | grep -q ' libgcc-8-dev'") == 0
and os.system(
"LC_ALL=C apt policy libgcc-8-dev | grep Candidate | grep -q rpi"
)
== 0
):
logger.info(
"Attempting to fix the build-essential / libc6-dev / libgcc-8-dev hell ..."
)
os.system("cp /var/lib/dpkg/status /root/dpkg_status.bkp")
# This removes the dependency to build-essential from $app-ynh-deps
os.system(
"perl -i~ -0777 -pe 's/(Package: .*-ynh-deps\\n(.+:.+\\n)+Depends:.*)(build-essential, ?)(.*)/$1$4/g' /var/lib/dpkg/status"
)
self.apt_install(
"build-essential-"
) # Note the '-' suffix to mean that we actually want to remove the packages
os.system(
"LC_ALL=C DEBIAN_FRONTEND=noninteractive APT_LISTCHANGES_FRONTEND=none apt autoremove --assume-yes"
)
self.apt_install(
"gcc-8- libgcc-8-dev- equivs"
) # Note the '-' suffix to mean that we actually want to remove the packages .. we also explicitly add 'equivs' to the list because sometimes apt is dumb and will derp about it
#
# Main upgrade
#
logger.info(m18n.n("migration_0021_main_upgrade"))
apps_packages = self.get_apps_equivs_packages()
self.hold(apps_packages)
tools_upgrade(target="system", allow_yunohost_upgrade=False)
if self.debian_major_version() == N_CURRENT_DEBIAN:
raise YunohostError("migration_0021_still_on_buster_after_main_upgrade")
# Force explicit install of php7.4-fpm and other old 'default' dependencies
# that are now only in Recommends
#
# Also, we need to install php7.4 equivalents of other php7.3 dependencies.
# For example, Nextcloud may depend on php7.3-zip, and after the php pool migration
# to autoupgrade Nextcloud to 7.4, it will need the php7.4-zip to work.
# The following list is based on an ad-hoc analysis of php deps found in the
# app ecosystem, with a known equivalent on php7.4.
#
# This is kinda a dirty hack as it doesnt properly update the *-ynh-deps virtual packages
# with the proper list of dependencies, and the dependencies install this way
# will get flagged as 'manually installed'.
#
# We'll probably want to do something during the Bullseye->Bookworm migration to re-flag
# these as 'auto' so they get autoremoved if not needed anymore.
# Also hopefully by then we'll have manifestv2 (maybe) and will be able to use
# the apt resource mecanism to regenerate the *-ynh-deps virtual packages ;)
php73packages_suffixes = [
"apcu",
"bcmath",
"bz2",
"dom",
"gmp",
"igbinary",
"imagick",
"imap",
"mbstring",
"memcached",
"mysqli",
"mysqlnd",
"pgsql",
"redis",
"simplexml",
"soap",
"sqlite3",
"ssh2",
"tidy",
"xml",
"xmlrpc",
"xsl",
"zip",
]
cmd = (
"apt show '*-ynh-deps' 2>/dev/null"
" | grep Depends"
f" | grep -o -E \"php7.3-({'|'.join(php73packages_suffixes)})\""
" | sort | uniq"
" | sed 's/php7.3/php7.4/g'"
" || true"
)
basephp74packages_to_install = [
"php7.4-fpm",
"php7.4-common",
"php7.4-ldap",
"php7.4-intl",
"php7.4-mysql",
"php7.4-gd",
"php7.4-curl",
"php-php-gettext",
]
php74packages_to_install = basephp74packages_to_install + [
f.strip() for f in check_output(cmd).split("\n") if f.strip()
]
ret = self.apt_install(
f"{' '.join(php74packages_to_install)} "
"$(dpkg --list | grep ynh-deps | awk '{print $2}') "
"-o Dpkg::Options::='--force-confmiss'"
)
if ret != 0:
raise YunohostError(
"Failed to force the install of php dependencies ?", raw_msg=True
)
# Clean the mess
logger.info(m18n.n("migration_0021_cleaning_up"))
os.system(
"LC_ALL=C DEBIAN_FRONTEND=noninteractive APT_LISTCHANGES_FRONTEND=none apt autoremove --assume-yes"
)
os.system("apt clean --assume-yes")
#
# Stupid hack for stupid dnsmasq not picking up its new init.d script then breaking everything ...
# https://forum.yunohost.org/t/20676
#
if os.path.exists("/etc/init.d/dnsmasq.dpkg-dist"):
logger.info("Copying new version for /etc/init.d/dnsmasq ...")
os.system("cp /etc/init.d/dnsmasq.dpkg-dist /etc/init.d/dnsmasq")
#
# Yunohost upgrade
#
logger.info(m18n.n("migration_0021_yunohost_upgrade"))
self.unhold(apps_packages)
cmd = "LC_ALL=C"
cmd += " DEBIAN_FRONTEND=noninteractive"
cmd += " APT_LISTCHANGES_FRONTEND=none"
cmd += " apt dist-upgrade "
cmd += " --quiet -o=Dpkg::Use-Pty=0 --fix-broken --dry-run"
cmd += " | grep -q 'ynh-deps'"
logger.info("Simulating upgrade...")
if os.system(cmd) == 0:
raise YunohostError(
"The upgrade cannot be completed, because some app dependencies would need to be removed?",
raw_msg=True,
)
postupgradecmds = f"apt-mark auto {' '.join(basephp74packages_to_install)}\n"
postupgradecmds += "rm -f /usr/sbin/policy-rc.d\n"
postupgradecmds += "echo 'Restarting nginx...' >&2\n"
postupgradecmds += "systemctl restart nginx\n"
tools_upgrade(target="system", postupgradecmds=postupgradecmds)
def debian_major_version(self):
# The python module "platform" and lsb_release are not reliable because
# on some setup, they may still return Release=9 even after upgrading to
# buster ... (Apparently this is related to OVH overriding some stuff
# 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 | head -n 1 | tr '\"' ' ' | cut -d ' ' -f2"
)
)
def yunohost_major_version(self):
return int(get_ynh_package_version("yunohost")["version"].split(".")[0])
def check_assertions(self):
# Be on buster (10.x) and yunohost 4.x
# NB : we do both check to cover situations where the upgrade crashed
# in the middle and debian version could be > 9.x but yunohost package
# would still be in 3.x...
if (
not self.debian_major_version() == N_CURRENT_DEBIAN
and not self.yunohost_major_version() == N_CURRENT_YUNOHOST
):
try:
# Here we try to find the previous migration log, which should be somewhat recent and be at least 10k (we keep the biggest one)
maybe_previous_migration_log_id = check_output(
"cd /var/log/yunohost/categories/operation && find -name '*migrate*.log' -size +10k -mtime -100 -exec ls -s {} \\; | sort -n | tr './' ' ' | awk '{print $2}' | tail -n 1"
)
if maybe_previous_migration_log_id:
logger.info(
f"NB: the previous migration log id seems to be {maybe_previous_migration_log_id}. You can share it with the support team with : sudo yunohost log share {maybe_previous_migration_log_id}"
)
except Exception:
# Yeah it's not that important ... it's to simplify support ...
pass
raise YunohostError("migration_0021_not_buster2")
# Have > 1 Go free space on /var/ ?
if free_space_in_directory("/var/") / (1024**3) < 1.0:
raise YunohostError("migration_0021_not_enough_free_space")
# Have > 70 MB free space on /var/ ?
if free_space_in_directory("/boot/") / (1024**2) < 70.0:
raise YunohostError(
"/boot/ has less than 70MB available. This will probably trigger a crash during the upgrade because a new kernel needs to be installed. Please look for advice on the forum on how to remove old, unused kernels to free up some space in /boot/.",
raw_msg=True,
)
# Check system is up to date
# (but we don't if 'bullseye' is already in the sources.list ...
# which means maybe a previous upgrade crashed and we're re-running it)
if os.path.exists("/etc/apt/sources.list") and " bullseye " not in read_file(
"/etc/apt/sources.list"
):
tools_update(target="system")
upgradable_system_packages = list(_list_upgradable_apt_packages())
upgradable_system_packages = [
package["name"] for package in upgradable_system_packages
]
upgradable_system_packages = set(upgradable_system_packages)
# Lime2 have hold packages to avoid ethernet instability
# See https://github.com/YunoHost/arm-images/commit/b4ef8c99554fd1a122a306db7abacc4e2f2942df
lime2_hold_packages = set(
[
"armbian-firmware",
"armbian-bsp-cli-lime2",
"linux-dtb-current-sunxi",
"linux-image-current-sunxi",
"linux-u-boot-lime2-current",
"linux-image-next-sunxi",
]
)
if upgradable_system_packages - lime2_hold_packages:
raise YunohostError("migration_0021_system_not_fully_up_to_date")
@property
def disclaimer(self):
# Avoid having a super long disclaimer + uncessary check if we ain't
# on buster / yunohost 4.x anymore
# NB : we do both check to cover situations where the upgrade crashed
# in the middle and debian version could be >= 10.x but yunohost package
# would still be in 4.x...
if (
not self.debian_major_version() == N_CURRENT_DEBIAN
and not self.yunohost_major_version() == N_CURRENT_YUNOHOST
):
return None
# Get list of problematic apps ? I.e. not official or community+working
problematic_apps = unstable_apps()
problematic_apps = "".join(["\n - " + app for app in problematic_apps])
# Manually modified files ? (c.f. yunohost service regen-conf)
modified_files = manually_modified_files()
modified_files = "".join(["\n - " + f for f in modified_files])
message = m18n.n("migration_0021_general_warning")
message = (
"N.B.: This migration has been tested by the community over the last few months but has only been declared stable recently. If your server hosts critical services and if you are not too confident with debugging possible issues, we recommend you to wait a little bit more while we gather more feedback and polish things up. If on the other hand you are relatively confident with debugging small issues that may arise, you are encouraged to run this migration ;)! You can read about remaining known issues and feedback from the community here: https://forum.yunohost.org/t/20590\n\n"
+ message
)
if problematic_apps:
message += "\n\n" + m18n.n(
"migration_0021_problematic_apps_warning",
problematic_apps=problematic_apps,
)
if modified_files:
message += "\n\n" + m18n.n(
"migration_0021_modified_files", manually_modified_files=modified_files
)
return message
def patch_apt_sources_list(self):
sources_list = glob.glob("/etc/apt/sources.list.d/*.list")
if os.path.exists("/etc/apt/sources.list"):
sources_list.append("/etc/apt/sources.list")
# This :
# - replace single 'buster' occurence by 'bulleye'
# - comments lines containing "backports"
# - replace 'buster/updates' by 'bullseye/updates' (or same with -)
# Special note about the security suite:
# https://www.debian.org/releases/bullseye/amd64/release-notes/ch-information.en.html#security-archive
for f in sources_list:
command = (
f"sed -i {f} "
"-e 's@ buster @ bullseye @g' "
"-e '/backports/ s@^#*@#@' "
"-e 's@ buster/updates @ bullseye-security @g' "
"-e 's@ buster-@ bullseye-@g' "
)
os.system(command)
def get_apps_equivs_packages(self):
command = (
"dpkg --get-selections"
" | grep -v deinstall"
" | awk '{print $1}'"
" | { grep 'ynh-deps$' || true; }"
)
output = check_output(command)
return output.split("\n") if output else []
def hold(self, packages):
for package in packages:
os.system(f"apt-mark hold {package}")
def unhold(self, packages):
for package in packages:
os.system(f"apt-mark unhold {package}")
def apt_install(self, cmd):
def is_relevant(line):
return "Reading database ..." not in line.rstrip()
callbacks = (
lambda l: (
logger.info("+ " + l.rstrip() + "\r")
if _apt_log_line_is_relevant(l)
else logger.debug(l.rstrip() + "\r")
),
lambda l: (
logger.warning(l.rstrip())
if _apt_log_line_is_relevant(l)
else logger.debug(l.rstrip())
),
)
cmd = (
"LC_ALL=C DEBIAN_FRONTEND=noninteractive APT_LISTCHANGES_FRONTEND=none apt install --quiet -o=Dpkg::Use-Pty=0 --fix-broken --assume-yes "
+ cmd
)
logger.debug("Running: %s" % cmd)
return call_async_output(cmd, callbacks, shell=True)
def patch_yunohost_conflicts(self):
#
# This is a super dirty hack to remove the conflicts from yunohost's debian/control file
# Those conflicts are there to prevent mistakenly upgrading critical packages
# such as dovecot, postfix, nginx, openssl, etc... usually related to mistakenly
# using backports etc.
#
# The hack consists in savagely removing the conflicts directly in /var/lib/dpkg/status
#
# We only patch the conflict if we're on yunohost 4.x
if self.yunohost_major_version() != N_CURRENT_YUNOHOST:
return
conflicts = check_output("dpkg-query -s yunohost | grep '^Conflicts:'").strip()
if conflicts:
# We want to keep conflicting with apache/bind9 tho
new_conflicts = "Conflicts: apache2, bind9"
command = (
f"sed -i /var/lib/dpkg/status -e 's@{conflicts}@{new_conflicts}@g'"
)
logger.debug(f"Running: {command}")
os.system(command)

View file

@ -0,0 +1,94 @@
import os
import glob
from shutil import copy2
from moulinette.utils.log import getActionLogger
from yunohost.app import _is_installed
from yunohost.utils.legacy import _patch_legacy_php_versions_in_settings
from yunohost.tools import Migration
from yunohost.service import _run_service_command
logger = getActionLogger("yunohost.migration")
OLDPHP_POOLS = "/etc/php/7.3/fpm/pool.d"
NEWPHP_POOLS = "/etc/php/7.4/fpm/pool.d"
OLDPHP_SOCKETS_PREFIX = "/run/php/php7.3-fpm"
NEWPHP_SOCKETS_PREFIX = "/run/php/php7.4-fpm"
# Because of synapse é_è
OLDPHP_SOCKETS_PREFIX2 = "/run/php7.3-fpm"
NEWPHP_SOCKETS_PREFIX2 = "/run/php7.4-fpm"
MIGRATION_COMMENT = (
"; YunoHost note : this file was automatically moved from {}".format(OLDPHP_POOLS)
)
class MyMigration(Migration):
"Migrate php7.3-fpm 'pool' conf files to php7.4"
dependencies = ["migrate_to_bullseye"]
def run(self):
# Get list of php7.3 pool files
oldphp_pool_files = glob.glob("{}/*.conf".format(OLDPHP_POOLS))
# Keep only basenames
oldphp_pool_files = [os.path.basename(f) for f in oldphp_pool_files]
# Ignore the "www.conf" (default stuff, probably don't want to touch it ?)
oldphp_pool_files = [f for f in oldphp_pool_files if f != "www.conf"]
for pf in oldphp_pool_files:
# Copy the files to the php7.3 pool
src = "{}/{}".format(OLDPHP_POOLS, pf)
dest = "{}/{}".format(NEWPHP_POOLS, pf)
copy2(src, dest)
# Replace the socket prefix if it's found
c = "sed -i -e 's@{}@{}@g' {}".format(
OLDPHP_SOCKETS_PREFIX, NEWPHP_SOCKETS_PREFIX, dest
)
os.system(c)
c = "sed -i -e 's@{}@{}@g' {}".format(
OLDPHP_SOCKETS_PREFIX2, NEWPHP_SOCKETS_PREFIX2, dest
)
os.system(c)
# Also add a comment that it was automatically moved from php7.3
# (for human traceability and backward migration)
c = "sed -i '1i {}' {}".format(MIGRATION_COMMENT, dest)
os.system(c)
app_id = os.path.basename(pf)[: -len(".conf")]
if _is_installed(app_id):
_patch_legacy_php_versions_in_settings(
"/etc/yunohost/apps/%s/" % app_id
)
nginx_conf_files = glob.glob("/etc/nginx/conf.d/*.d/%s.conf" % app_id)
for nf in nginx_conf_files:
# Replace the socket prefix if it's found
c = "sed -i -e 's@{}@{}@g' {}".format(
OLDPHP_SOCKETS_PREFIX, NEWPHP_SOCKETS_PREFIX, nf
)
os.system(c)
c = "sed -i -e 's@{}@{}@g' {}".format(
OLDPHP_SOCKETS_PREFIX2, NEWPHP_SOCKETS_PREFIX2, nf
)
os.system(c)
os.system(
"rm /etc/logrotate.d/php7.3-fpm"
) # We remove this otherwise the logrotate cron will be unhappy
# Reload/restart the php pools
os.system("systemctl stop php7.3-fpm")
os.system("systemctl disable php7.3-fpm")
_run_service_command("restart", "php7.4-fpm")
_run_service_command("enable", "php7.4-fpm")
# Reload nginx
_run_service_command("reload", "nginx")

View file

@ -1,21 +1,21 @@
import subprocess
import time
import os
from logging import getLogger
from moulinette import m18n
from yunohost.utils.error import YunohostError, YunohostValidationError
from moulinette.utils.log import getActionLogger
from yunohost.tools import Migration
from yunohost.utils.system import free_space_in_directory, space_used_by_directory
logger = getLogger("yunohost.migration")
logger = getActionLogger("yunohost.migration")
class MyMigration(Migration):
"Migrate DBs from Postgresql 13 to 15 after migrating to Bookworm"
"Migrate DBs from Postgresql 11 to 13 after migrating to Bullseye"
dependencies = ["migrate_to_bookworm"]
dependencies = ["migrate_to_bullseye"]
def run(self):
if (
@ -27,37 +27,37 @@ class MyMigration(Migration):
logger.info("No YunoHost app seem to require postgresql... Skipping!")
return
if not self.package_is_installed("postgresql-13"):
logger.warning(m18n.n("migration_0029_postgresql_13_not_installed"))
if not self.package_is_installed("postgresql-11"):
logger.warning(m18n.n("migration_0023_postgresql_11_not_installed"))
return
if not self.package_is_installed("postgresql-15"):
raise YunohostValidationError("migration_0029_postgresql_15_not_installed")
if not self.package_is_installed("postgresql-13"):
raise YunohostValidationError("migration_0023_postgresql_13_not_installed")
# Make sure there's a 13 cluster
# Make sure there's a 11 cluster
try:
self.runcmd("pg_lsclusters | grep -q '^13 '")
self.runcmd("pg_lsclusters | grep -q '^11 '")
except Exception:
logger.warning(
"It looks like there's not active 13 cluster, so probably don't need to run this migration"
"It looks like there's not active 11 cluster, so probably don't need to run this migration"
)
return
if not space_used_by_directory(
"/var/lib/postgresql/13"
"/var/lib/postgresql/11"
) > free_space_in_directory("/var/lib/postgresql"):
raise YunohostValidationError(
"migration_0029_not_enough_space", path="/var/lib/postgresql/"
"migration_0023_not_enough_space", path="/var/lib/postgresql/"
)
self.runcmd("systemctl stop postgresql")
time.sleep(3)
self.runcmd(
"LC_ALL=C pg_dropcluster --stop 15 main || true"
) # We do not trigger an exception if the command fails because that probably means cluster 15 doesn't exists, which is fine because it's created during the pg_upgradecluster)
"LC_ALL=C pg_dropcluster --stop 13 main || true"
) # We do not trigger an exception if the command fails because that probably means cluster 13 doesn't exists, which is fine because it's created during the pg_upgradecluster)
time.sleep(3)
self.runcmd("LC_ALL=C pg_upgradecluster -m upgrade 13 main -v 15")
self.runcmd("LC_ALL=C pg_dropcluster --stop 13 main")
self.runcmd("LC_ALL=C pg_upgradecluster -m upgrade 11 main")
self.runcmd("LC_ALL=C pg_dropcluster --stop 11 main")
self.runcmd("systemctl start postgresql")
def package_is_installed(self, package_name):

View file

@ -1,16 +1,16 @@
import os
from logging import getLogger
from moulinette import m18n
from moulinette.utils.log import getActionLogger
from moulinette.utils.process import call_async_output
from yunohost.tools import Migration, tools_migrations_state
from moulinette.utils.filesystem import rm
logger = getLogger("yunohost.migration")
logger = getActionLogger("yunohost.migration")
VENV_REQUIREMENTS_SUFFIX = ".requirements_backup_for_bookworm_upgrade.txt"
VENV_REQUIREMENTS_SUFFIX = ".requirements_backup_for_bullseye_upgrade.txt"
def extract_app_from_venv_path(venv_path):
@ -56,28 +56,28 @@ class MyMigration(Migration):
"""
ignored_python_apps = [
"diacamma", # Does an ugly sed in the sites-packages/django_auth_ldap3_ad
"kresus", # uses virtualenv instead of venv, with --system-site-packages (?)
"librephotos", # runs a setup.py ? not sure pip freeze / pip install -r requirements.txt is gonna be equivalent ..
"mautrix", # install stuff from a .tar.gz
"microblogpub", # uses poetry ? x_x
"mopidy", # applies a custom patch?
"motioneye", # install stuff from a .tar.gz
"pgadmin", # bunch of manual patches
"searxng", # uses --system-site-packages ?
"synapse", # specific stuff for ARM to prevent local compiling etc
"matrix-synapse", # synapse is actually installed in /opt/yunohost/matrix-synapse because ... yeah ...
"tracim", # pip install -e .
"weblate", # weblate settings are .. inside the venv T_T
"calibreweb",
"django-for-runners",
"ffsync",
"jupiterlab",
"librephotos",
"mautrix",
"mediadrop",
"mopidy",
"pgadmin",
"tracim",
"synapse",
"matrix-synapse",
"weblate",
]
dependencies = ["migrate_to_bookworm"]
dependencies = ["migrate_to_bullseye"]
state = None
def is_pending(self):
if not self.state:
self.state = tools_migrations_state()["migrations"].get(
"0030_rebuild_python_venv_in_bookworm", "pending"
"0024_rebuild_python_venv", "pending"
)
return self.state == "pending"
@ -121,15 +121,15 @@ class MyMigration(Migration):
else:
rebuild_apps.append(app_corresponding_to_venv)
msg = m18n.n("migration_0030_rebuild_python_venv_in_bookworm_disclaimer_base")
msg = m18n.n("migration_0024_rebuild_python_venv_disclaimer_base")
if rebuild_apps:
msg += "\n\n" + m18n.n(
"migration_0030_rebuild_python_venv_in_bookworm_disclaimer_rebuild",
"migration_0024_rebuild_python_venv_disclaimer_rebuild",
rebuild_apps="\n - " + "\n - ".join(rebuild_apps),
)
if ignored_apps:
msg += "\n\n" + m18n.n(
"migration_0030_rebuild_python_venv_in_bookworm_disclaimer_ignored",
"migration_0024_rebuild_python_venv_disclaimer_ignored",
ignored_apps="\n - " + "\n - ".join(ignored_apps),
)
@ -151,7 +151,7 @@ class MyMigration(Migration):
rm(venv + VENV_REQUIREMENTS_SUFFIX)
logger.info(
m18n.n(
"migration_0030_rebuild_python_venv_in_bookworm_broken_app",
"migration_0024_rebuild_python_venv_broken_app",
app=app_corresponding_to_venv,
)
)
@ -159,7 +159,7 @@ class MyMigration(Migration):
logger.info(
m18n.n(
"migration_0030_rebuild_python_venv_in_bookworm_in_progress",
"migration_0024_rebuild_python_venv_in_progress",
app=app_corresponding_to_venv,
)
)
@ -178,7 +178,7 @@ class MyMigration(Migration):
if status != 0:
logger.error(
m18n.n(
"migration_0030_rebuild_python_venv_in_bookworm_failed",
"migration_0024_rebuild_python_venv_failed",
app=app_corresponding_to_venv,
)
)

View file

@ -0,0 +1,42 @@
import os
from yunohost.utils.error import YunohostError
from moulinette.utils.log import getActionLogger
from moulinette.utils.filesystem import read_json, write_to_yaml
from yunohost.tools import Migration
from yunohost.utils.legacy import translate_legacy_settings_to_configpanel_settings
logger = getActionLogger("yunohost.migration")
SETTINGS_PATH = "/etc/yunohost/settings.yml"
OLD_SETTINGS_PATH = "/etc/yunohost/settings.json"
class MyMigration(Migration):
"Migrate old global settings to the new ConfigPanel global settings"
dependencies = ["migrate_to_bullseye"]
def run(self):
if not os.path.exists(OLD_SETTINGS_PATH):
return
try:
old_settings = read_json(OLD_SETTINGS_PATH)
except Exception as e:
raise YunohostError(f"Can't open setting file : {e}", raw_msg=True)
settings = {
translate_legacy_settings_to_configpanel_settings(k).split(".")[-1]: v[
"value"
]
for k, v in old_settings.items()
}
if settings.get("smtp_relay_host"):
settings["smtp_relay_enabled"] = True
# Here we don't use settings_set() from settings.py to prevent
# Questions to be asked when one run the migration from CLI.
write_to_yaml(SETTINGS_PATH, settings)

Some files were not shown because too many files have changed in this diff Show more