Sunil Mohan Adapa 436060fecb
apache: Allow URL diagnostics to work with redirects
Upon access of an app URL, it may redirect to another URL that is configured in
app settings. This new URL could only be accessed on IPv4 or IPv6 only. When
curl is invoked with the IP address version of a different kind, the access
fails. In such cases, tell the diagnostics methods not the restrict to a
particular address type.

Tests:

- Unit tests pass.

- All of transmission's diagnostics tests pass. The URL tests show that they
have been performed on a particular IP address type.

Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
2022-05-21 19:55:05 -04:00

191 lines
6.2 KiB
Python

# SPDX-License-Identifier: AGPL-3.0-or-later
"""
App component for other apps to use Apache configuration functionality.
"""
import re
import subprocess
from django.utils.text import format_lazy
from django.utils.translation import gettext_lazy
from plinth import action_utils, actions, app
class Webserver(app.LeaderComponent):
"""Component to enable/disable Apache configuration."""
def __init__(self, component_id, web_name, kind='config', urls=None,
expect_redirects=False):
"""Initialize the web server component.
component_id should be a unique ID across all components of an app and
across all components.
web_name is the primary part of the configuration file path which must
be enabled/disabled by this component.
kind is the type of Apache configuration being enabled/disabled. This
must be 'config' for a configuration in /etc/apache/conf-available/,
'module' for configuration in /etc/apache2/mods-available/, 'site' for
configuration in /etc/apache2/sites-available/.
urls is a list of URLs over which a HTTP services will be available due
to this component. This list is only used for running diagnostics.
"""
super().__init__(component_id)
self.web_name = web_name
self.kind = kind
self.urls = urls or []
self.expect_redirects = expect_redirects
def is_enabled(self):
"""Return whether the Apache configuration is enabled."""
return action_utils.webserver_is_enabled(self.web_name, kind=self.kind)
def enable(self):
"""Enable the Apache configuration."""
actions.superuser_run(
'apache', ['enable', '--name', self.web_name, '--kind', self.kind])
def disable(self):
"""Disable the Apache configuration."""
actions.superuser_run(
'apache',
['disable', '--name', self.web_name, '--kind', self.kind])
def diagnose(self):
"""Check if the web path is accessible by clients.
See :py:meth:`plinth.app.Component.diagnose`.
"""
results = []
for url in self.urls:
if '{host}' in url:
results.extend(
diagnose_url_on_all(
url, check_certificate=False,
expect_redirects=self.expect_redirects))
else:
results.append(diagnose_url(url, check_certificate=False))
return results
class Uwsgi(app.LeaderComponent):
"""Component to enable/disable uWSGI configuration."""
def __init__(self, component_id, uwsgi_name):
"""Initialize the uWSGI component.
component_id should be a unique ID across all components of an app and
across all components.
uwsgi_name is the primary part of the configuration file path which
must be enabled/disabled by this component.
"""
super().__init__(component_id)
self.uwsgi_name = uwsgi_name
def is_enabled(self):
"""Return whether the uWSGI configuration is enabled."""
return action_utils.uwsgi_is_enabled(self.uwsgi_name) \
and action_utils.service_is_enabled('uwsgi')
def enable(self):
"""Enable the uWSGI configuration."""
actions.superuser_run('apache',
['uwsgi-enable', '--name', self.uwsgi_name])
def disable(self):
"""Disable the uWSGI configuration."""
actions.superuser_run('apache',
['uwsgi-disable', '--name', self.uwsgi_name])
def is_running(self):
"""Return whether the uWSGI daemon is running with configuration."""
return action_utils.uwsgi_is_enabled(self.uwsgi_name) \
and action_utils.service_is_running('uwsgi')
def diagnose_url(url, kind=None, env=None, check_certificate=True,
extra_options=None, wrapper=None, expected_output=None):
"""Run a diagnostic on whether a URL is accessible.
Kind can be '4' for IPv4 or '6' for IPv6.
"""
result = check_url(url, kind, env, check_certificate, extra_options,
wrapper, expected_output)
if kind:
template = gettext_lazy('Access URL {url} on tcp{kind}')
testname = format_lazy(template, url=url, kind=kind)
else:
template = gettext_lazy('Access URL {url}')
testname = format_lazy(template, url=url)
return [testname, result]
def diagnose_url_on_all(url, expect_redirects=False, **kwargs):
"""Run a diagnostic on whether a URL is accessible."""
results = []
for address in action_utils.get_addresses():
current_url = url.format(host=address['url_address'])
diagnose_kwargs = dict(kwargs)
if not expect_redirects:
diagnose_kwargs.setdefault('kind', address['kind'])
results.append(diagnose_url(current_url, **diagnose_kwargs))
return results
def check_url(url, kind=None, env=None, check_certificate=True,
extra_options=None, wrapper=None, expected_output=None):
"""Check whether a URL is accessible."""
command = ['curl', '--location', '-f', '-w', '%{response_code}']
if kind == '6':
# extract zone index
match = re.match(r'(.*://)\[(.*)%(?P<zone>.*)\](.*)', url)
if match:
command = command + ['--interface', match.group('zone')]
url = '{0}[{1}]{2}'.format(*match.group(1, 2, 4))
command.append(url)
if wrapper:
command.insert(0, wrapper)
if not check_certificate:
command.append('-k')
if extra_options:
command.extend(extra_options)
if kind:
command.append({'4': '-4', '6': '-6'}[kind])
try:
process = subprocess.run(command, env=env, check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
result = 'passed'
if expected_output and expected_output not in process.stdout.decode():
result = 'failed'
except subprocess.CalledProcessError as exception:
result = 'failed'
# Authorization failed is a success
if exception.stdout.decode().strip() in ('401', '405'):
result = 'passed'
except FileNotFoundError:
result = 'error'
return result