1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248
|
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Configure Apache web server."""
import glob
import os
import pathlib
import re
from plinth import action_utils
from plinth.actions import privileged
def _get_sort_key_of_version(version):
"""Return the sort key for a given version string.
Simple implementation hoping that PHP Apache module version numbers will be
simple.
"""
parts = []
for part in version.split('.'):
try:
parts.append(int(part))
except ValueError:
parts.append(part)
return parts
def _sort_versions(versions):
"""Return a list of sorted version strings."""
return sorted(versions, key=_get_sort_key_of_version, reverse=True)
def _disable_mod_php(webserver):
"""Disable all mod_php versions.
Idempotent and harmless if all or no PHP modules are identified.
Problematic if only some modules are found.
"""
paths = glob.glob('/etc/apache2/mods-available/php*.conf')
versions = []
for path in paths:
match = re.search(r'\/php(.*)\.conf$', path)
if match:
versions.append(match[1])
versions = _sort_versions(versions)
for version in versions:
webserver.disable('php' + version, kind='module')
@privileged
def setup(old_version: int):
"""Setup Apache configuration."""
# Regenerate the snakeoil self-signed SSL certificate. This is so that
# FreedomBox images don't all have the same certificate. When FreedomBox
# package is installed via apt, don't regenerate. When upgrading to newer
# version of Apache FreedomBox app and setting up for the first time don't
# regenerate.
if action_utils.is_disk_image() and old_version == 0:
action_utils.run([
'make-ssl-cert', 'generate-default-snakeoil', '--force-overwrite'
], check=True)
# In case the certificate has been removed after ssl-cert is installed
# on a fresh Debian machine.
elif not os.path.exists('/etc/ssl/certs/ssl-cert-snakeoil.pem'):
action_utils.run(['make-ssl-cert', 'generate-default-snakeoil'],
check=True)
with action_utils.WebserverChange() as webserver:
# Disable mod_php as we have switched to mod_fcgi + php-fpm. Disable
# before switching away from mpm_prefork otherwise switching fails due
# dependency.
_disable_mod_php(webserver)
# set the prefork worker model
webserver.disable('mpm_worker', kind='module')
webserver.disable('mpm_prefork', kind='module')
webserver.enable('mpm_event', kind='module')
# enable miscellaneous modules.
webserver.enable('proxy', kind='module')
webserver.enable('proxy_http', kind='module')
webserver.enable('proxy_fcgi', kind='module')
webserver.enable('proxy_html', kind='module')
webserver.enable('rewrite', kind='module')
webserver.enable('macro', kind='module')
webserver.enable('expires', kind='module')
# Disable logging into files, use FreedomBox configured systemd logging
webserver.disable('other-vhosts-access-log', kind='config')
# Disable /server-status page to avoid leaking private info.
webserver.disable('status', kind='module')
# Enable HTTP/2 protocol
webserver.enable('http2', kind='module')
# Enable shared object cache needed for OSCP stapling. Needed by
# mod_ssl.
webserver.enable('socache_shmcb', kind='module')
# switch to mod_ssl from mod_gnutls
webserver.disable('gnutls', kind='module')
webserver.enable('ssl', kind='module')
# enable mod_alias for RedirectMatch
webserver.enable('alias', kind='module')
# enable mod_headers for HSTS
webserver.enable('headers', kind='module')
# Various modules for authentication/authorization
webserver.enable('authnz_ldap', kind='module')
webserver.enable('auth_pubtkt', kind='module')
# enable some critical modules to avoid restart while installing
# FreedomBox applications.
webserver.disable('cgi', kind='module') # For process MPMs
webserver.enable('cgid', kind='module') # For threaded MPMs
webserver.enable('proxy_uwsgi', kind='module')
webserver.enable('proxy_wstunnel', kind='module')
# enable configuration for PHP-FPM
webserver.enable('php-fpm-freedombox', kind='config')
# enable users to share files uploaded to ~/public_html
webserver.enable('userdir', kind='module')
# enable WebDAV protocol. Used by feather wiki and potentially by other
# apps and file sharing.
webserver.enable('dav', kind='module')
webserver.enable('dav_fs', kind='module')
# setup freedombox site
webserver.enable('freedombox', kind='config')
webserver.enable('freedombox-tls', kind='config')
# enable serving Debian javascript libraries
webserver.enable('javascript-common', kind='config')
# default sites
webserver.disable('000-default', kind='site')
webserver.disable('default-tls', kind='site')
webserver.disable('default-ssl', kind='site')
webserver.disable('plinth', kind='site')
webserver.disable('plinth-ssl', kind='site')
webserver.enable('freedombox-default', kind='site')
# TODO: Check that the (name, kind) is a managed by FreedomBox before
# performing operation.
@privileged
def enable(name: str, kind: str):
"""Enable an Apache site/config/module."""
_assert_kind(kind)
action_utils.webserver_enable(name, kind)
@privileged
def disable(name: str, kind: str):
"""Disable an Apache site/config/module."""
_assert_kind(kind)
action_utils.webserver_disable(name, kind)
def _assert_kind(kind: str):
"""Raise and exception if kind parameter has an unexpected value."""
if kind not in ('site', 'config', 'module'):
raise ValueError('Invalid value for parameter kind')
@privileged
def link_root(domain: str, name: str):
"""Link the Apache site root configuration to app configuration."""
if '/' in domain or '/' in name:
raise ValueError('Invalid domain or name')
target_config = f'{name}.conf'
include_root = pathlib.Path('/etc/apache2/includes/')
config = include_root / f'{domain}-include-freedombox.conf'
config.unlink(missing_ok=True)
config.symlink_to(target_config)
action_utils.service_reload('apache2')
@privileged
def unlink_root(domain: str):
"""Unlink the Apache site root configuration from app configuration."""
if '/' in domain:
raise ValueError('Invalid domain')
include_root = pathlib.Path('/etc/apache2/includes/')
config = include_root / f'{domain}-include-freedombox.conf'
if not config.is_symlink():
return # Does not exist or not a symlink
config.unlink()
action_utils.service_reload('apache2')
@privileged
def uwsgi_enable(name: str):
"""Enable uWSGI configuration and reload."""
action_utils.uwsgi_enable(name)
@privileged
def uwsgi_disable(name: str):
"""Disable uWSGI configuration and reload."""
action_utils.uwsgi_disable(name)
@privileged
def domain_setup(domain: str):
"""Add site specific configuration for a domain."""
if '/' in domain:
raise ValueError('Invalid domain')
path = pathlib.Path('/etc/apache2/sites-available/')
path = path / (domain + '.conf')
configuration = 'Use FreedomBoxTLSSiteMacro {domain}\n'
if path.is_file():
return # File already exists. Assume it to be correct one.
path.write_text(configuration.format(domain=domain))
with action_utils.WebserverChange() as webserver:
webserver.enable('freedombox-tls-site-macro', kind='config')
webserver.enable(domain, kind='site')
@privileged
def domain_remove(domain: str):
"""Remove site specific configuration for a domain."""
if '/' in domain:
raise ValueError('Invalid domain')
with action_utils.WebserverChange() as webserver:
webserver.disable(domain, kind='site')
path = pathlib.Path('/etc/apache2/sites-available/')
path = path / (domain + '.conf')
path.unlink(missing_ok=True)
|