mirror of
https://github.com/certbot/certbot.git
synced 2026-07-28 08:05:31 +02:00
Merge pull request #1395 from sagi/hsts
apache: add general http-header enhacement [needs revision]
This commit is contained in:
@@ -122,7 +122,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
self.parser = None
|
||||
self.version = version
|
||||
self.vhosts = None
|
||||
self._enhance_func = {"redirect": self._enable_redirect}
|
||||
self._enhance_func = {"redirect": self._enable_redirect,
|
||||
"ensure-http-header": self._set_http_header}
|
||||
|
||||
@property
|
||||
def mod_ssl_conf(self):
|
||||
@@ -739,7 +740,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
############################################################################
|
||||
def supported_enhancements(self): # pylint: disable=no-self-use
|
||||
"""Returns currently supported enhancements."""
|
||||
return ["redirect"]
|
||||
return ["redirect", "ensure-http-header"]
|
||||
|
||||
def enhance(self, domain, enhancement, options=None):
|
||||
"""Enhance configuration.
|
||||
@@ -766,6 +767,73 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
logger.warn("Failed %s for %s", enhancement, domain)
|
||||
raise
|
||||
|
||||
def _set_http_header(self, ssl_vhost, header_substring):
|
||||
"""Enables header that is identified by header_substring on ssl_vhost.
|
||||
|
||||
If the header identified by header_substring is not already set,
|
||||
a new Header directive is placed in ssl_vhost's configuration with
|
||||
arguments from: constants.HTTP_HEADER[header_substring]
|
||||
|
||||
.. note:: This function saves the configuration
|
||||
|
||||
:param ssl_vhost: Destination of traffic, an ssl enabled vhost
|
||||
:type ssl_vhost: :class:`~letsencrypt_apache.obj.VirtualHost`
|
||||
|
||||
:param header_substring: string that uniquely identifies a header.
|
||||
e.g: Strict-Transport-Security, Upgrade-Insecure-Requests.
|
||||
:type str
|
||||
|
||||
:returns: Success, general_vhost (HTTP vhost)
|
||||
:rtype: (bool, :class:`~letsencrypt_apache.obj.VirtualHost`)
|
||||
|
||||
:raises .errors.PluginError: If no viable HTTP host can be created or
|
||||
set with header header_substring.
|
||||
|
||||
"""
|
||||
if "headers_module" not in self.parser.modules:
|
||||
self.enable_mod("headers")
|
||||
|
||||
# Check if selected header is already set
|
||||
self._verify_no_matching_http_header(ssl_vhost, header_substring)
|
||||
|
||||
# Add directives to server
|
||||
self.parser.add_dir(ssl_vhost.path, "Header",
|
||||
constants.HEADER_ARGS[header_substring])
|
||||
|
||||
self.save_notes += ("Adding %s header to ssl vhost in %s\n" %
|
||||
(header_substring, ssl_vhost.filep))
|
||||
|
||||
self.save()
|
||||
logger.info("Adding %s header to ssl vhost in %s", header_substring,
|
||||
ssl_vhost.filep)
|
||||
|
||||
def _verify_no_matching_http_header(self, ssl_vhost, header_substring):
|
||||
"""Checks to see if an there is an existing Header directive that
|
||||
contains the string header_substring.
|
||||
|
||||
:param ssl_vhost: vhost to check
|
||||
:type vhost: :class:`~letsencrypt_apache.obj.VirtualHost`
|
||||
|
||||
:param header_substring: string that uniquely identifies a header.
|
||||
e.g: Strict-Transport-Security, Upgrade-Insecure-Requests.
|
||||
:type str
|
||||
|
||||
:returns: boolean
|
||||
:rtype: (bool)
|
||||
|
||||
:raises errors.PluginEnhancementAlreadyPresent When header
|
||||
header_substring exists
|
||||
|
||||
"""
|
||||
header_path = self.parser.find_dir("Header", None, start=ssl_vhost.path)
|
||||
if header_path:
|
||||
# "Existing Header directive for virtualhost"
|
||||
pat = '(?:[ "]|^)(%s)(?:[ "]|$)' % (header_substring.lower())
|
||||
for match in header_path:
|
||||
if re.search(pat, self.aug.get(match).lower()):
|
||||
raise errors.PluginEnhancementAlreadyPresent(
|
||||
"Existing %s header" % (header_substring))
|
||||
|
||||
def _enable_redirect(self, ssl_vhost, unused_options):
|
||||
"""Redirect all equivalent HTTP traffic to ssl_vhost.
|
||||
|
||||
@@ -835,8 +903,12 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
:param vhost: vhost to check
|
||||
:type vhost: :class:`~letsencrypt_apache.obj.VirtualHost`
|
||||
|
||||
:raises errors.PluginError: When another redirection exists
|
||||
:raises errors.PluginEnhancementAlreadyPresent: When the exact
|
||||
letsencrypt redirection WriteRule exists in virtual host.
|
||||
|
||||
errors.PluginError: When there exists directives that may hint
|
||||
other redirection. (TODO: We should not throw a PluginError,
|
||||
but that's for an other PR.)
|
||||
"""
|
||||
rewrite_path = self.parser.find_dir(
|
||||
"RewriteRule", None, start=vhost.path)
|
||||
@@ -853,7 +925,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
rewrite_path, constants.REWRITE_HTTPS_ARGS):
|
||||
if self.aug.get(match) != arg:
|
||||
raise errors.PluginError("Unknown Existing RewriteRule")
|
||||
raise errors.PluginError(
|
||||
|
||||
raise errors.PluginEnhancementAlreadyPresent(
|
||||
"Let's Encrypt has already enabled redirection")
|
||||
|
||||
def _create_redirect_vhost(self, ssl_vhost):
|
||||
|
||||
@@ -27,3 +27,15 @@ AUGEAS_LENS_DIR = pkg_resources.resource_filename(
|
||||
REWRITE_HTTPS_ARGS = [
|
||||
"^", "https://%{SERVER_NAME}%{REQUEST_URI}", "[L,QSA,R=permanent]"]
|
||||
"""Apache rewrite rule arguments used for redirections to https vhost"""
|
||||
|
||||
|
||||
HSTS_ARGS = ["always", "set", "Strict-Transport-Security",
|
||||
"\"max-age=31536000; includeSubDomains\""]
|
||||
"""Apache header arguments for HSTS"""
|
||||
|
||||
UIR_ARGS = ["always", "set", "Content-Security-Policy",
|
||||
"upgrade-insecure-requests"]
|
||||
|
||||
HEADER_ARGS = {"Strict-Transport-Security": HSTS_ARGS,
|
||||
"Upgrade-Insecure-Requests": UIR_ARGS}
|
||||
|
||||
|
||||
@@ -630,6 +630,84 @@ class TwoVhost80Test(util.ApacheTest):
|
||||
errors.PluginError,
|
||||
self.config.enhance, "letsencrypt.demo", "unknown_enhancement")
|
||||
|
||||
@mock.patch("letsencrypt.le_util.run_script")
|
||||
@mock.patch("letsencrypt.le_util.exe_exists")
|
||||
def test_http_header_hsts(self, mock_exe, _):
|
||||
self.config.parser.update_runtime_variables = mock.Mock()
|
||||
self.config.parser.modules.add("mod_ssl.c")
|
||||
mock_exe.return_value = True
|
||||
|
||||
# This will create an ssl vhost for letsencrypt.demo
|
||||
self.config.enhance("letsencrypt.demo", "ensure-http-header",
|
||||
"Strict-Transport-Security")
|
||||
|
||||
self.assertTrue("headers_module" in self.config.parser.modules)
|
||||
|
||||
# Get the ssl vhost for letsencrypt.demo
|
||||
ssl_vhost = self.config.assoc["letsencrypt.demo"]
|
||||
|
||||
# These are not immediately available in find_dir even with save() and
|
||||
# load(). They must be found in sites-available
|
||||
hsts_header = self.config.parser.find_dir(
|
||||
"Header", None, ssl_vhost.path)
|
||||
|
||||
# four args to HSTS header
|
||||
self.assertEqual(len(hsts_header), 4)
|
||||
|
||||
def test_http_header_hsts_twice(self):
|
||||
self.config.parser.modules.add("mod_ssl.c")
|
||||
# skip the enable mod
|
||||
self.config.parser.modules.add("headers_module")
|
||||
|
||||
# This will create an ssl vhost for letsencrypt.demo
|
||||
self.config.enhance("encryption-example.demo", "ensure-http-header",
|
||||
"Strict-Transport-Security")
|
||||
|
||||
self.assertRaises(
|
||||
errors.PluginEnhancementAlreadyPresent,
|
||||
self.config.enhance, "encryption-example.demo", "ensure-http-header",
|
||||
"Strict-Transport-Security")
|
||||
|
||||
@mock.patch("letsencrypt.le_util.run_script")
|
||||
@mock.patch("letsencrypt.le_util.exe_exists")
|
||||
def test_http_header_uir(self, mock_exe, _):
|
||||
self.config.parser.update_runtime_variables = mock.Mock()
|
||||
self.config.parser.modules.add("mod_ssl.c")
|
||||
mock_exe.return_value = True
|
||||
|
||||
# This will create an ssl vhost for letsencrypt.demo
|
||||
self.config.enhance("letsencrypt.demo", "ensure-http-header",
|
||||
"Upgrade-Insecure-Requests")
|
||||
|
||||
self.assertTrue("headers_module" in self.config.parser.modules)
|
||||
|
||||
# Get the ssl vhost for letsencrypt.demo
|
||||
ssl_vhost = self.config.assoc["letsencrypt.demo"]
|
||||
|
||||
# These are not immediately available in find_dir even with save() and
|
||||
# load(). They must be found in sites-available
|
||||
uir_header = self.config.parser.find_dir(
|
||||
"Header", None, ssl_vhost.path)
|
||||
|
||||
# four args to HSTS header
|
||||
self.assertEqual(len(uir_header), 4)
|
||||
|
||||
def test_http_header_uir_twice(self):
|
||||
self.config.parser.modules.add("mod_ssl.c")
|
||||
# skip the enable mod
|
||||
self.config.parser.modules.add("headers_module")
|
||||
|
||||
# This will create an ssl vhost for letsencrypt.demo
|
||||
self.config.enhance("encryption-example.demo", "ensure-http-header",
|
||||
"Upgrade-Insecure-Requests")
|
||||
|
||||
self.assertRaises(
|
||||
errors.PluginEnhancementAlreadyPresent,
|
||||
self.config.enhance, "encryption-example.demo", "ensure-http-header",
|
||||
"Upgrade-Insecure-Requests")
|
||||
|
||||
|
||||
|
||||
@mock.patch("letsencrypt.le_util.run_script")
|
||||
@mock.patch("letsencrypt.le_util.exe_exists")
|
||||
def test_redirect_well_formed_http(self, mock_exe, _):
|
||||
@@ -670,7 +748,7 @@ class TwoVhost80Test(util.ApacheTest):
|
||||
self.config.parser.modules.add("rewrite_module")
|
||||
self.config.enhance("encryption-example.demo", "redirect")
|
||||
self.assertRaises(
|
||||
errors.PluginError,
|
||||
errors.PluginEnhancementAlreadyPresent,
|
||||
self.config.enhance, "encryption-example.demo", "redirect")
|
||||
|
||||
def test_unknown_rewrite(self):
|
||||
|
||||
Reference in New Issue
Block a user