File Manager
Viewing File: errors.py
# Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2020 All Rights Reserved
#
# Licensed under CLOUD LINUX LICENSE AGREEMENT
# http://cloudlinux.com/docs/LICENSE.TXT
import html
from urllib.parse import urlsplit
from clwpos import gettext as _
from clwpos.cl_wpos_exceptions import WposError
# Characters that could break out of the double-quoted ``href="%(url)s"``
# attribute or inject a tag in the anchor body. A url free of these is safe in
# the HTML anchor while remaining a valid plain-text value for the
# ``curl %(url)s`` fix_tip sink. A single quote ``'`` is intentionally NOT in
# this set: inside the double-quoted href attribute (and the anchor body) it is
# inert, yet it is a valid RFC-3986 sub-delim that legitimately appears in
# ``response.url`` (post-redirect target, checkers.py); rejecting it would blank
# a legitimate diagnostic url for no XSS gain.
_URL_BREAKING_CHARS = ('"', '<', '>')
def _safe_url(url):
"""Validate a caller-supplied url shared between the HTML templates and the
plain-text ``curl %(url)s`` fix_tip.
The same value feeds an HTML sink (the anchor in MESSAGE_TEMPLATE) and a
plain-text sink (the curl fix_tip), so it must not be html-escaped: that
would corrupt a legitimate url (e.g. ``?a=1&b=2`` -> ``?a=1&b=2``) in
the copy-pasteable command. Instead validate-and-reject: keep the url
byte-for-byte when its scheme is http/https and it contains no
attribute/tag-breaking characters; otherwise drop it to an empty string.
A scheme-validated url with no breaking chars is safe in the HTML anchor
(``&`` alone cannot break an attribute or inject script) and correct in the
curl command.
"""
if not isinstance(url, str):
return ''
# Reject raw whitespace / ASCII control bytes (CR, LF, tab, space, NUL,
# DEL, ...) up front. A real http/https url percent-encodes these, so a
# raw control byte is never a legitimate diagnostic url and must not land
# verbatim in the href / the plain-text ``curl %(url)s`` tip. (A double-
# quoted href attribute is not actually terminated by a newline -- only by
# ``"`` -- but a url validator should not pass control characters through,
# and it keeps urlsplit from having to cope with embedded controls.)
if any(ch.isspace() or ord(ch) < 0x20 or ord(ch) == 0x7f for ch in url):
return ''
scheme = urlsplit(url).scheme.lower()
if scheme not in ('http', 'https'):
return ''
if any(ch in url for ch in _URL_BREAKING_CHARS):
return ''
return url
# Context key whose value is a url shared between the HTML ``description`` and
# the plain-text ``curl %(url)s`` FIX_TIP. It must be validate-and-rejected
# (``_safe_url``) rather than html-escaped so the same byte-for-byte value stays
# correct in the copy-pasteable curl command while remaining safe in the anchor.
_URL_CONTEXT_KEY = 'url'
class WebsiteCheckError(WposError):
"""
Generic base class for all website post checks.
Every context value reaching the HTML ``description`` is neutralised here,
centrally, so no subclass, future key, or direct base-class raise (e.g. the
case-4 ``error_desc`` in wp_utils._process_post_check) can leak raw
caller/site-influenced markup into the panel HTML sink. The ``url`` key is
validate-and-rejected via ``_safe_url`` (kept raw-but-safe so it stays a
valid ``curl %(url)s`` command in the plain-text FIX_TIP); every other value
is html-escaped. ``self.context`` therefore holds values that are
simultaneously safe in the HTML ``description`` and intact enough for the
plain-text FIX_TIP (which only ever interpolates ``url``/``timeout``).
"""
def __init__(self, *, header, description, fix_tip, context, details=None):
super(WebsiteCheckError, self).__init__(
message=description,
context=self._neutralise_context(context)
)
self.header = header
self.fix_tip = fix_tip
self.details = details
@staticmethod
def _neutralise_context(context):
"""Return a copy of ``context`` safe for the HTML ``description`` sink:
the ``url`` value validate-and-rejected via ``_safe_url`` (so the same
value remains correct in the plain-text ``curl %(url)s`` FIX_TIP), and
every other value html-escaped. Escaping is harmless on server-built
values (paths, fixed ints render unchanged)."""
if not context:
return context
return {
key: _safe_url(value) if key == _URL_CONTEXT_KEY
else html.escape(str(value), quote=True)
for key, value in context.items()
}
class PostCheckRequestException(WebsiteCheckError):
HEADER = _('Http request failed')
MESSAGE_TEMPLATE = _(
'Fatal error on attempt to request website main page: '
'<a href="%(url)s" target="_blank">%(url)s</a>. '
'Error message: %(error)s')
FIX_TIP = _(
'Manually check website responds on http requests'
)
def __init__(self, url, error):
super(PostCheckRequestException, self).__init__(
header=self.HEADER,
description=self.MESSAGE_TEMPLATE,
context=dict(
url=url,
error=error
),
fix_tip=self.FIX_TIP
)
class WebsiteCheckBadHttpCode(WebsiteCheckError):
"""
Happens when we get unexpected status code from website.
Usually user must go check logs and fix this issue himself.
"""
HEADER = _('Bad http status code response')
MESSAGE_TEMPLATE = _(
'Unsuccessful attempt to request website main page: '
'<a href="%(url)s" target="_blank">%(url)s</a>. '
'Webserver responded with http status code %(http_code)s.')
FIX_TIP = _(
'Search your webserver logs for errors and manually '
'check that WordPress site is working properly.'
)
def __init__(self, url, http_code):
super(WebsiteCheckBadHttpCode, self).__init__(
header=self.HEADER,
description=self.MESSAGE_TEMPLATE,
context=dict(
url=url,
http_code=http_code
),
fix_tip=self.FIX_TIP
)
class WebsiteTimeout(WebsiteCheckError):
"""
Happens when website does not respond in hardcoded timeout.
"""
HEADER = _('Response timeout reached')
MESSAGE_TEMPLATE = _(
'Unsuccessful attempt to request website main page: '
'<a href="%(url)s" target="_blank">%(url)s</a>. '
'Webserver responds longer than %(timeout)s seconds '
'(most likely it is not working).')
FIX_TIP = _(
'Make sure that WordPress site is working properly and '
'specified url gives response in %(timeout)s seconds. '
'Search your webserver logs for errors that may cause the timeout issue.'
)
def __init__(self, url, timeout):
super(WebsiteTimeout, self).__init__(
header=self.HEADER,
description=self.MESSAGE_TEMPLATE,
context=dict(
url=url,
timeout=timeout
),
fix_tip=self.FIX_TIP
)
class WebsiteHttpsBroken(WebsiteCheckError):
"""
Happens when website has invalid
or outdated https certificate.
"""
HEADER = _('Broken https certificate')
MESSAGE_TEMPLATE = _(
'Unsuccessful attempt to request website main page: '
'<a href="%(url)s" target="_blank">%(url)s</a>. '
'Webserver redirected http request to https, but the last '
'one does not have a valid certificate.')
FIX_TIP = _(
'Make sure that https certificate is valid and up to date or '
'disable http to https redirection in your control panel.'
)
def __init__(self, url, details):
super(WebsiteHttpsBroken, self).__init__(
header=self.HEADER,
description=self.MESSAGE_TEMPLATE,
context=dict(
url=url
),
fix_tip=self.FIX_TIP,
details=details
)
class WebsiteNotResponding(WebsiteCheckError):
"""
Happens when website does not have working webserver at all.
"""
HEADER = _('Website it not responding')
MESSAGE_TEMPLATE = _(
'Unsuccessful attempt to request website main page: '
'<a href="%(url)s" target="_blank">%(url)s</a>. '
'Webserver did not return any response.')
FIX_TIP = _(
'Make sure that webserver is working and your website is accessible: '
'you can use `curl %(url)s` to check response.'
)
def __init__(self, url, details):
super(WebsiteNotResponding, self).__init__(
header=self.HEADER,
description=self.MESSAGE_TEMPLATE,
context=dict(
url=url
),
fix_tip=self.FIX_TIP,
details=details
)
class RollbackException(WposError):
MESSAGE_TEMPLATE = _(
"Action failed and rolled back. Here is why."
)
def __init__(self, *reasons: WebsiteCheckError):
self.errors = [
dict(
header=reason.header,
description=reason.message,
fix_tip=reason.fix_tip,
type='post_check',
context=reason.context,
details=reason.details
) for reason in reasons
]
super(RollbackException, self).__init__(message='rollback')
class PhpLogErrorsFound(WebsiteCheckError):
HEADER = _('Website server error')
MESSAGE_TEMPLATE = _(
'We found some errors in the website log file: %(error_log_path)s. '
'Here is some context: \n'
'%(log_record)s')
FIX_TIP = _(
'Check found errors and decide whether they are critical or not. '
'Contact your system administrator for help if needed.'
)
def __init__(self, error_log_path, log_record):
super(PhpLogErrorsFound, self).__init__(
header=self.HEADER,
description=self.MESSAGE_TEMPLATE,
context=dict(
error_log_path=error_log_path,
log_record=log_record
),
fix_tip=self.FIX_TIP
)
class CDNActivationFailed(WebsiteCheckError):
"""
Happens when website has invalid CDN configuration for some reason
"""
HEADER = _('CDN malfunction')
MESSAGE_TEMPLATE = _(
"CDN feature was not activated properly. "
"Changes were reverted and CDN module is now disabled.\n"
"Event is logged to file: '%(logger_path)s' with stdout and stderr recorded.")
FIX_TIP = _(
'Activate feature again. If the issue persists, '
'ignore error and check your website configuration '
'with enabled CDN feature. '
'Contact your hoster\'s support for help.'
)
def __init__(self, logger_path):
super(CDNActivationFailed, self).__init__(
header=self.HEADER,
description=self.MESSAGE_TEMPLATE,
fix_tip=self.FIX_TIP,
context=dict(logger_path=logger_path)
)
class JSCssCheckBadHttpCode(WebsiteCheckError):
"""
Happens when we get unexpected status code from website.
Usually user must go check logs and fix this issue himself.
"""
HEADER = _('Bad http status code response')
MESSAGE_TEMPLATE = _(
'Unsuccessful attempt to request website styles and scripts. '
'Webserver responded with http status code %(http_code)s.')
FIX_TIP = _(
'Search your webserver logs for errors and manually '
'check that WordPress site is working properly.'
)
def __init__(self, http_code):
super(JSCssCheckBadHttpCode, self).__init__(
header=self.HEADER,
description=self.MESSAGE_TEMPLATE,
context=dict(
http_code=http_code
),
fix_tip=self.FIX_TIP
)