1
0
mirror of https://github.com/certbot/certbot.git synced 2026-01-26 07:41:33 +03:00

Start work on multivhost support in Apache

* get through parsing

* not slice

* add mult vhost per file

* idx line backwards

* blocks be wrong

* always close ifmod

* let's not mess up indexes

* don't double add multi

* fix some lint, only dedupe multi

* tests

* fix lint

* in progress bit flip

* try to pick the right vhost

* take Dominic's suggestion

* don't redo search

* add ancestor

* we now support multiple vhosts

* yay

* add docstrings
This commit is contained in:
Noah Swartz
2016-07-26 15:57:11 -07:00
committed by Brad Warren
parent b3116af5b4
commit f57f35b1dd
10 changed files with 453 additions and 71 deletions

View File

@@ -138,6 +138,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
self._enhance_func = {"redirect": self._enable_redirect,
"ensure-http-header": self._set_http_header,
"staple-ocsp": self._enable_ocsp_stapling}
self._skeletons = {}
@property
def mod_ssl_conf(self):
@@ -589,6 +590,9 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
if realpath not in vhost_paths.keys():
vhs.append(new_vhost)
vhost_paths[realpath] = new_vhost.filep
elif (realpath in vhost_paths.keys()
and new_vhost.path.endswith("]") and new_vhost not in vhs):
vhs.append(new_vhost)
elif realpath == new_vhost.filep:
# Prefer "real" vhost paths instead of symlinked ones
# ex: sites-enabled/vh.conf -> sites-available/vh.conf
@@ -792,20 +796,21 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
avail_fp = nonssl_vhost.filep
ssl_fp = self._get_ssl_vhost_path(avail_fp)
self._copy_create_ssl_vhost_skeleton(avail_fp, ssl_fp)
vhost_num = -1
if nonssl_vhost.path.endswith("]"):
# augeas doesn't zero index for whatever reason
vhost_num = int(nonssl_vhost.path[-2]) - 1
self._copy_create_ssl_vhost_skeleton(avail_fp, ssl_fp, vhost_num)
# Reload augeas to take into account the new vhost
self.aug.load()
# Get Vhost augeas path for new vhost
vh_p = self.aug.match("/files%s//* [label()=~regexp('%s')]" %
(self._escape(ssl_fp), parser.case_i("VirtualHost")))
if len(vh_p) != 1:
logger.error("Error: should only be one vhost in %s", avail_fp)
raise errors.PluginError("Currently, we only support "
"configurations with one vhost per file")
else:
# This simplifies the process
vh_p = vh_p[0]
temp_vh = vh_p[0]
if self._skeletons[ssl_fp]:
temp_vh = vh_p[len(self._skeletons[ssl_fp]) -1]
vh_p = temp_vh
# Update Addresses
self._update_ssl_vhosts_addrs(vh_p)
@@ -822,6 +827,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
# We know the length is one because of the assertion above
# Create the Vhost object
ssl_vhost = self._create_vhost(vh_p)
ssl_vhost.ancestor = nonssl_vhost
self.vhosts.append(ssl_vhost)
# NOTE: Searches through Augeas seem to ruin changes to directives
@@ -875,7 +881,38 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
# Sift line if it redirects the request to a HTTPS site
return target.startswith("https://")
def _copy_create_ssl_vhost_skeleton(self, avail_fp, ssl_fp):
def _section_blocks(self, blocks):
"""A helper function for _create_block_segments that makes
a list of line numbers to not include in the return.
:param list blocks: A list of indexes of where vhosts start and end.
"""
out = []
while len(blocks) > 1:
start = blocks[0]
end = blocks[1] + 1
out += range(start, end)
blocks = blocks[2:]
return out
def _create_block_segments(self, orig_file_list, vhost_num):
"""A helper function for _copy_create_ssl_vhost_skeleton
that slices the appropriate vhost from the origin conf file.
:param list orig_file_list: the original file converted to a list of strings.
"param int vhost_num: Which vhost the vhost is in the origin multivhost file.
"""
blocks = [idx for idx, line in enumerate(orig_file_list)
if line.lstrip().startswith("<VirtualHost")
or line.lstrip().startswith("</VirtualHost")]
blocks = blocks[:vhost_num*2] + blocks[(vhost_num*2)+2:]
out = self._section_blocks(blocks)
return [line for idx, line in enumerate(orig_file_list)
if idx not in out]
def _copy_create_ssl_vhost_skeleton(self, avail_fp, ssl_fp, vhost_num): # pylint: disable=too-many-branches,too-many-locals,too-many-statements
"""Copies over existing Vhost with IfModule mod_ssl.c> skeleton.
:param str avail_fp: Pointer to the original available non-ssl vhost
@@ -891,65 +928,77 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
try:
with open(avail_fp, "r") as orig_file:
with open(ssl_fp, "w") as new_file:
new_file.write("<IfModule mod_ssl.c>\n")
orig_file_list = [line for line in orig_file]
if vhost_num != -1:
orig_file_list = self._create_block_segments(orig_file_list, vhost_num)
comment = ("# Some rewrite rules in this file were "
"disabled on your HTTPS site,\n"
"# because they have the potential to create "
"redirection loops.\n")
if ssl_fp in self._skeletons:
bit = "a"
self._skeletons[ssl_fp].append(avail_fp)
else:
bit = "w"
self._skeletons[ssl_fp] = [avail_fp]
for line in orig_file:
A = line.lstrip().startswith("RewriteCond")
B = line.lstrip().startswith("RewriteRule")
with open(ssl_fp, bit) as new_file:
new_file.write("<IfModule mod_ssl.c>\n")
if not (A or B):
new_file.write(line)
continue
comment = ("# Some rewrite rules in this file were "
"disabled on your HTTPS site,\n"
"# because they have the potential to create "
"redirection loops.\n")
# A RewriteRule that doesn't need filtering
if B and not self._sift_rewrite_rule(line):
new_file.write(line)
continue
orig_file_list = iter(orig_file_list)
for line in orig_file_list:
A = line.lstrip().startswith("RewriteCond")
B = line.lstrip().startswith("RewriteRule")
# A RewriteRule that does need filtering
if B and self._sift_rewrite_rule(line):
if not (A or B):
new_file.write(line)
continue
# A RewriteRule that doesn't need filtering
if B and not self._sift_rewrite_rule(line):
new_file.write(line)
continue
# A RewriteRule that does need filtering
if B and self._sift_rewrite_rule(line):
if not sift:
new_file.write(comment)
sift = True
new_file.write("# " + line)
continue
# We save RewriteCond(s) and their corresponding
# RewriteRule in 'chunk'.
# We then decide whether we comment out the entire
# chunk based on its RewriteRule.
chunk = []
if A:
chunk.append(line)
line = next(orig_file_list)
# RewriteCond(s) must be followed by one RewriteRule
while not line.lstrip().startswith("RewriteRule"):
chunk.append(line)
line = next(orig_file_list)
# Now, current line must start with a RewriteRule
chunk.append(line)
if self._sift_rewrite_rule(line):
if not sift:
new_file.write(comment)
sift = True
new_file.write("# " + line)
new_file.write(''.join(
['# ' + l for l in chunk]))
continue
else:
new_file.write(''.join(chunk))
continue
# We save RewriteCond(s) and their corresponding
# RewriteRule in 'chunk'.
# We then decide whether we comment out the entire
# chunk based on its RewriteRule.
chunk = []
if A:
chunk.append(line)
line = next(orig_file)
# RewriteCond(s) must be followed by one RewriteRule
while not line.lstrip().startswith("RewriteRule"):
chunk.append(line)
line = next(orig_file)
# Now, current line must start with a RewriteRule
chunk.append(line)
if self._sift_rewrite_rule(line):
if not sift:
new_file.write(comment)
sift = True
new_file.write(''.join(
['# ' + l for l in chunk]))
continue
else:
new_file.write(''.join(chunk))
continue
new_file.write("</IfModule>\n")
new_file.write("</IfModule>\n")
except IOError:
logger.fatal("Error writing/reading to file in make_vhost_ssl")
raise errors.PluginError("Unable to write/read in make_vhost_ssl")
@@ -962,6 +1011,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
"the potential to create redirection loops.".format(avail_fp,
ssl_fp),
reporter.MEDIUM_PRIORITY)
self.aug.set("/augeas/files%s/mtime" %(self._escape(ssl_fp)), "0")
self.aug.set("/augeas/files%s/mtime" %(self._escape(avail_fp)), "0")
def _update_ssl_vhosts_addrs(self, vh_path):
ssl_addrs = set()
@@ -1008,12 +1059,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
self.parser.add_dir(vh_path, "Include", self.mod_ssl_conf)
def _add_servername_alias(self, target_name, vhost):
fp = self._escape(vhost.filep)
vh_p = self.aug.match("/files%s//* [label()=~regexp('%s')]" %
(fp, parser.case_i("VirtualHost")))
if not vh_p:
return
vh_path = vh_p[0]
vh_path = vhost.path
if (self.parser.find_dir("ServerName", target_name,
start=vh_path, exclude=False) or
self.parser.find_dir("ServerAlias", target_name,
@@ -1508,6 +1554,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
def _get_http_vhost(self, ssl_vhost):
"""Find appropriate HTTP vhost for ssl_vhost."""
# First candidate vhosts filter
if ssl_vhost.ancestor:
return ssl_vhost.ancestor
candidate_http_vhs = [
vhost for vhost in self.vhosts if not vhost.ssl
]

View File

@@ -123,7 +123,7 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods
strip_name = re.compile(r"^(?:.+://)?([^ :$]*)")
def __init__(self, filep, path, addrs, ssl, enabled, name=None,
aliases=None, modmacro=False):
aliases=None, modmacro=False, ancestor=None):
# pylint: disable=too-many-arguments
"""Initialize a VH."""
@@ -135,6 +135,7 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods
self.ssl = ssl
self.enabled = enabled
self.modmacro = modmacro
self.ancestor = ancestor
def get_names(self):
"""Return a set of all names."""

View File

@@ -655,11 +655,6 @@ class MultipleVhostsTest(util.ApacheTest):
len(self.config.parser.find_dir(
directive, None, self.vh_truth[1].path, False)), 0)
def test_make_vhost_ssl_extra_vhs(self):
self.config.aug.match = mock.Mock(return_value=["p1", "p2"])
self.assertRaises(
errors.PluginError, self.config.make_vhost_ssl, self.vh_truth[0])
def test_make_vhost_ssl_bad_write(self):
mock_open = mock.mock_open()
# This calls open
@@ -1345,6 +1340,65 @@ class AugeasVhostsTest(util.ApacheTest):
self.config.choose_vhost(name)
self.assertEqual(mock_select.call_count, 0)
class MultiVhostsTest(util.ApacheTest):
"""Test vhosts with illegal names dependant on augeas version."""
# pylint: disable=protected-access
def setUp(self): # pylint: disable=arguments-differ
td = "debian_apache_2_4/multi_vhosts"
cr = "debian_apache_2_4/multi_vhosts/apache2"
vr = "debian_apache_2_4/multi_vhosts/apache2/sites-available"
super(MultiVhostsTest, self).setUp(test_dir=td,
config_root=cr,
vhost_root=vr)
self.config = util.get_apache_configurator(
self.config_path, self.vhost_path, self.config_dir, self.work_dir)
self.vh_truth = util.get_vh_truth(
self.temp_dir, "debian_apache_2_4/multi_vhosts")
def tearDown(self):
shutil.rmtree(self.temp_dir)
shutil.rmtree(self.config_dir)
shutil.rmtree(self.work_dir)
def test_make_vhost_ssl(self):
ssl_vhost = self.config.make_vhost_ssl(self.vh_truth[1])
self.assertEqual(
ssl_vhost.filep,
os.path.join(self.config_path, "sites-available",
"default-le-ssl.conf"))
self.assertEqual(ssl_vhost.path,
"/files" + ssl_vhost.filep + "/IfModule/VirtualHost")
self.assertEqual(len(ssl_vhost.addrs), 1)
self.assertEqual(set([obj.Addr.fromstring("*:443")]), ssl_vhost.addrs)
self.assertEqual(ssl_vhost.name, "banana.vomit.com")
self.assertTrue(ssl_vhost.ssl)
self.assertFalse(ssl_vhost.enabled)
self.assertTrue(self.config.parser.find_dir(
"SSLCertificateFile", None, ssl_vhost.path, False))
self.assertTrue(self.config.parser.find_dir(
"SSLCertificateKeyFile", None, ssl_vhost.path, False))
self.assertEqual(self.config.is_name_vhost(self.vh_truth[1]),
self.config.is_name_vhost(ssl_vhost))
def test_make_2nd_vhost_ssl(self):
_ = self.config.make_vhost_ssl(self.vh_truth[0])
_ = self.config.make_vhost_ssl(self.vh_truth[1])
self.assertEqual(
len(self.config._skeletons[self.config._get_ssl_vhost_path(self.vh_truth[0].filep)]), 2)
def test_cover_is_stupid_and_I_hate_it(self):
http_vhost = obj.VirtualHost(None, None, None, False, False, name="Noah")
ssl_vhost = obj.VirtualHost(None, None, None, False, False, name="Noah")
self.config.vhosts.append(http_vhost)
self.assertEqual(self.config._get_http_vhost(ssl_vhost), http_vhost)
if __name__ == "__main__":
unittest.main() # pragma: no cover

View File

@@ -0,0 +1,196 @@
# This is the main Apache server configuration file. It contains the
# configuration directives that give the server its instructions.
# See http://httpd.apache.org/docs/2.4/ for detailed information about
# the directives and /usr/share/doc/apache2/README.Debian about Debian specific
# hints.
#
#
# Summary of how the Apache 2 configuration works in Debian:
# The Apache 2 web server configuration in Debian is quite different to
# upstream's suggested way to configure the web server. This is because Debian's
# default Apache2 installation attempts to make adding and removing modules,
# virtual hosts, and extra configuration directives as flexible as possible, in
# order to make automating the changes and administering the server as easy as
# possible.
# It is split into several files forming the configuration hierarchy outlined
# below, all located in the /etc/apache2/ directory:
#
# /etc/apache2/
# |-- apache2.conf
# | `-- ports.conf
# |-- mods-enabled
# | |-- *.load
# | `-- *.conf
# |-- conf-enabled
# | `-- *.conf
# `-- sites-enabled
# `-- *.conf
#
#
# * apache2.conf is the main configuration file (this file). It puts the pieces
# together by including all remaining configuration files when starting up the
# web server.
#
# * ports.conf is always included from the main configuration file. It is
# supposed to determine listening ports for incoming connections which can be
# customized anytime.
#
# * Configuration files in the mods-enabled/, conf-enabled/ and sites-enabled/
# directories contain particular configuration snippets which manage modules,
# global configuration fragments, or virtual host configurations,
# respectively.
#
# They are activated by symlinking available configuration files from their
# respective *-available/ counterparts. These should be managed by using our
# helpers a2enmod/a2dismod, a2ensite/a2dissite and a2enconf/a2disconf. See
# their respective man pages for detailed information.
#
# * The binary is called apache2. Due to the use of environment variables, in
# the default configuration, apache2 needs to be started/stopped with
# /etc/init.d/apache2 or apache2ctl. Calling /usr/bin/apache2 directly will not
# work with the default configuration.
# Global configuration
#
# The accept serialization lock file MUST BE STORED ON A LOCAL DISK.
#
Mutex file:${APACHE_LOCK_DIR} default
#
# PidFile: The file in which the server should record its process
# identification number when it starts.
# This needs to be set in /etc/apache2/envvars
#
PidFile ${APACHE_PID_FILE}
#
# Timeout: The number of seconds before receives and sends time out.
#
Timeout 300
#
# KeepAlive: Whether or not to allow persistent connections (more than
# one request per connection). Set to "Off" to deactivate.
#
KeepAlive On
#
# MaxKeepAliveRequests: The maximum number of requests to allow
# during a persistent connection. Set to 0 to allow an unlimited amount.
# We recommend you leave this number high, for maximum performance.
#
MaxKeepAliveRequests 100
#
# KeepAliveTimeout: Number of seconds to wait for the next request from the
# same client on the same connection.
#
KeepAliveTimeout 5
# These need to be set in /etc/apache2/envvars
User ${APACHE_RUN_USER}
Group ${APACHE_RUN_GROUP}
#
# HostnameLookups: Log the names of clients or just their IP addresses
# e.g., www.apache.org (on) or 204.62.129.132 (off).
# The default is off because it'd be overall better for the net if people
# had to knowingly turn this feature on, since enabling it means that
# each client request will result in AT LEAST one lookup request to the
# nameserver.
#
HostnameLookups Off
# ErrorLog: The location of the error log file.
# If you do not specify an ErrorLog directive within a <VirtualHost>
# container, error messages relating to that virtual host will be
# logged here. If you *do* define an error logfile for a <VirtualHost>
# container, that host's errors will be logged there and not here.
#
ErrorLog ${APACHE_LOG_DIR}/error.log
#
# LogLevel: Control the severity of messages logged to the error_log.
# Available values: trace8, ..., trace1, debug, info, notice, warn,
# error, crit, alert, emerg.
# It is also possible to configure the log level for particular modules, e.g.
# "LogLevel info ssl:warn"
#
LogLevel warn
# Include module configuration:
IncludeOptional mods-enabled/*.load
IncludeOptional mods-enabled/*.conf
# Include list of ports to listen on
Include ports.conf
# Sets the default security model of the Apache2 HTTPD server. It does
# not allow access to the root filesystem outside of /usr/share and /var/www.
# The former is used by web applications packaged in Debian,
# the latter may be used for local directories served by the web server. If
# your system is serving content from a sub-directory in /srv you must allow
# access here, or in any related virtual host.
<Directory />
Options FollowSymLinks
AllowOverride None
Require all denied
</Directory>
<Directory /usr/share>
AllowOverride None
Require all granted
</Directory>
<Directory /var/>
Options Indexes FollowSymLinks
AllowOverride None
Require all granted
</Directory>
# AccessFileName: The name of the file to look for in each directory
# for additional configuration directives. See also the AllowOverride
# directive.
#
AccessFileName .htaccess
#
# The following lines prevent .htaccess and .htpasswd files from being
# viewed by Web clients.
#
<FilesMatch "^\.ht">
Require all denied
</FilesMatch>
# The following directives define some format nicknames for use with
# a CustomLog directive.
#
# These deviate from the Common Log Format definitions in that they use %O
# (the actual bytes sent including headers) instead of %b (the size of the
# requested file), because the latter makes it impossible to detect partial
# requests.
#
# Note that the use of %{X-Forwarded-For}i instead of %h is not recommended.
# Use mod_remoteip instead.
#
LogFormat "%v:%p %h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" vhost_combined
LogFormat "%h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" combined
LogFormat "%h %l %u %t \"%r\" %>s %O" common
LogFormat "%{Referer}i -> %U" referer
LogFormat "%{User-agent}i" agent
# Include of directories ignores editors' and dpkg's backup files,
# see README.Debian for details.
# Include generic snippets of statements
IncludeOptional conf-enabled/*.conf
# Include the virtual host configurations:
IncludeOptional sites-enabled/*.conf
# vim: syntax=apache ts=4 sw=4 sts=4 sr noet

View File

@@ -0,0 +1,29 @@
# envvars - default environment variables for apache2ctl
# this won't be correct after changing uid
unset HOME
# for supporting multiple apache2 instances
if [ "${APACHE_CONFDIR##/etc/apache2-}" != "${APACHE_CONFDIR}" ] ; then
SUFFIX="-${APACHE_CONFDIR##/etc/apache2-}"
else
SUFFIX=
fi
# Since there is no sane way to get the parsed apache2 config in scripts, some
# settings are defined via environment variables and then used in apache2ctl,
# /etc/init.d/apache2, /etc/logrotate.d/apache2, etc.
export APACHE_RUN_USER=www-data
export APACHE_RUN_GROUP=www-data
# temporary state file location. This might be changed to /run in Wheezy+1
export APACHE_PID_FILE=/var/run/apache2/apache2$SUFFIX.pid
export APACHE_RUN_DIR=/var/run/apache2$SUFFIX
export APACHE_LOCK_DIR=/var/lock/apache2$SUFFIX
# Only /var/log/apache2 is handled by /etc/logrotate.d/apache2.
export APACHE_LOG_DIR=/var/log/apache2$SUFFIX
## The locale used by some modules like mod_dav
export LANG=C
export LANG

View File

@@ -0,0 +1,15 @@
# If you just change the port or add more ports here, you will likely also
# have to change the VirtualHost statement in
# /etc/apache2/sites-enabled/000-default.conf
Listen 80
<IfModule ssl_module>
Listen 443
</IfModule>
<IfModule mod_gnutls.c>
Listen 443
</IfModule>
# vim: syntax=apache ts=4 sw=4 sts=4 sr noet

View File

@@ -0,0 +1,22 @@
<VirtualHost *:80>
ServerName banana.vomit.net
ServerAdmin webmaster@localhost
DocumentRoot /var/www/html
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>
<VirtualHost *:80>
ServerName banana.vomit.com
ServerAdmin webmaster@localhost
DocumentRoot /var/www/html
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>
# vim: syntax=apache ts=4 sw=4 sts=4 sr noet

View File

@@ -164,5 +164,22 @@ def get_vh_truth(temp_dir, config_name):
set([obj.Addr.fromstring("10.2.3.4:443")]), True, True,
"ocspvhost.com")]
return vh_truth
if config_name == "debian_apache_2_4/multi_vhosts":
prefix = os.path.join(
temp_dir, config_name, "apache2/sites-available")
aug_pre = "/files" + prefix
vh_truth = [
obj.VirtualHost(
os.path.join(prefix, "default.conf"),
os.path.join(aug_pre, "default.conf/VirtualHost[1]"),
set([obj.Addr.fromstring("*:80")]),
False, True, "ip-172-30-0-17"),
obj.VirtualHost(
os.path.join(prefix, "default.conf"),
os.path.join(aug_pre, "default.conf/VirtualHost[2]"),
set([obj.Addr.fromstring("*:80")]),
False, True, "banana.vomit.com")]
return vh_truth
return None # pragma: no cover

View File

@@ -491,7 +491,7 @@ class Reverter(object):
else:
logger.warning(
"File: %s - Could not be found to be deleted %s - "
"LE probably shut down unexpectedly",
"Certbot probably shut down unexpectedly",
os.linesep, path)
except (IOError, OSError):
logger.fatal(