File: privileged.py

package info (click to toggle)
freedombox 26.2
  • links: PTS, VCS
  • area: main
  • in suites: forky
  • size: 82,976 kB
  • sloc: python: 48,504; javascript: 1,736; xml: 481; makefile: 290; sh: 167; php: 32
file content (248 lines) | stat: -rw-r--r-- 8,307 bytes parent folder | download | duplicates (3)
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)