Repository: certbot/certbot

Issue:
the apache plugin adds an unconditional redirect inside the :443 vhost
The apache plugin adds an unconditional redirect inside the :443 vhost

I had to comment out these lines or else the webaddress just kept looping

RewriteEngine on
RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [L,QSA,R=permanent]



Discussion:
I personally haven't been following this issue closely, but do you have any thoughts @sagi?

What version of Apache are you running @timir?

Server version: Apache/2.4.10 (Debian)

@timir, did you have a http (:80)  vhost prior to configuring with letsencrypt ?

Yeah, I did have a http(:80) vhost conf file prior to configuring with letsencrypt.

I'm on it.

@timir uses 2.4, and for apache versions greater than 2.3.9  we don't use the L flag, but the END flag. 
Id est, @timir probably used an older version of letsencrypt. 

It seems that @timir's client returns a `ssl_vhost` when executing `self._get_http_vhost(ssl_vhost)` (and not a general http vhost). 

Unfortunately, I can't reproduce the scenario. 

@timir, if it is not too demanding, can you please elaborate more on the prior states of both http and https vhosts? (The best thing would be to have the actual vhosts prior to configuring with letsencrypt, but it is probable you don't have them anymore)

@sagi the original vhost conf file can be seen on this pastie http://pastie.org/10659144

after running letsencrypt it creates another vhost file with a name like somedomain-le-ssl.conf
http://pastie.org/10659148

thanks @timir! I've been able to reproduce the scenario. It occurs on an edge case - there exists a http vhost for a domain with a redirection rewriterule and a ssl vhost, for the same domain, doesn't exist. Then the client creates a new ssl vhost and mistakenly puts a redirection rewriterule.

Btw, the reason the `L` flag is there and not the `END` flag is - `letsencrypt-auto` uses `pip install letsencrypt` which is letsencrypt 0.1.1.

After some debugging I found the problem - it's not the `redirection enhancement` but the `_copy_create_ssl_vhost_skeleton` function. When no ssl vhost present it just copies everything from the `http vhost` to the newly created `ssl vhost`.


Resolving patch:
diff --git a/letsencrypt-apache/letsencrypt_apache/configurator.py b/letsencrypt-apache/letsencrypt_apache/configurator.py
--- a/letsencrypt-apache/letsencrypt_apache/configurator.py
+++ b/letsencrypt-apache/letsencrypt_apache/configurator.py
@@ -697,6 +697,47 @@ def _get_ssl_vhost_path(self, non_ssl_vh_fp):
         else:
             return non_ssl_vh_fp + self.conf("le_vhost_ext")
 
+    def _sift_line(self, line):
+        """ Decides whether a line shouldn't be copied from a http vhost to a
+            SSL vhost.
+
+        A canonical example of when sifting a line is required:
+        When the http vhost contains a RewriteRule that unconditionally redirects
+        any request to the https version of the same site.
+        e.g: RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [L,QSA,R=permanent]
+        Copying the above line to the ssl vhost would cause a redirection loop.
+
+        :param str line: a line extracted from the http vhost config file.
+
+        :returns: True - don't copy line from http vhost to SSL vhost.
+        :rtype: (bool)
+        """
+
+        rewrite_rule = "RewriteRule"
+        if line.lstrip()[:len(rewrite_rule)] != rewrite_rule:
+            return False
+
+        # line starts with RewriteRule.
+
+        # According to: http://httpd.apache.org/docs/2.4/rewrite/flags.html
+        # The syntax of a RewriteRule is:
+        # RewriteRule pattern target [Flag1,Flag2,Flag3]
+        # i.e. target is required, so it must exist.
+        target = line.split()[2].strip()
+
+        # target may be surrounded with double quotes
+        if len(target) > 0 and target[0] == target[-1] == "\"":
+            target = target[1:-1]
+
+        https_prefix = "https://"
+        if len(target) < len(https_prefix):
+            return False
+
+        if target[:len(https_prefix)] == https_prefix:
+            return True
+
+        return False
+
     def _copy_create_ssl_vhost_skeleton(self, avail_fp, ssl_fp):
         """Copies over existing Vhost with IfModule mod_ssl.c> skeleton.
 
@@ -714,9 +755,27 @@ def _copy_create_ssl_vhost_skeleton(self, avail_fp, ssl_fp):
             with open(avail_fp, "r") as orig_file:
                 with open(ssl_fp, "w") as new_file:
                     new_file.write("<IfModule mod_ssl.c>\n")
+                    sift = False
+
                     for line in orig_file:
-                        new_file.write(line)
+                        if self._sift_line(line):
+                            if not sift:
+                                new_file.write("# The following rewrite rules "
+                                "were *not* enabled on your HTTPS site,\n"
+                                "# because they have the potential to create "
+                                "redirection loops:\n")
+                            sift = True
+                            new_file.write("# " + line)
+                        else:
+                            new_file.write(line)
+
                     new_file.write("</IfModule>\n")
+
+                    if sift:
+                        logger.warn("Some rewrite rules were *not* enabled on "
+                                "your HTTPS site, because they have the "
+                                "potential to create redirection loops.")
+
         except IOError:
             logger.fatal("Error writing/reading to file in make_vhost_ssl")
             raise errors.PluginError("Unable to write/read in make_vhost_ssl")


Test patch:
diff --git a/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py b/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py
--- a/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py
+++ b/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py
@@ -857,7 +857,8 @@ def test_redirect_with_existing_rewrite(self, mock_exe, _):
 
         # Create a preexisting rewrite rule
         self.config.parser.add_dir(
-            self.vh_truth[3].path, "RewriteRule", ["Unknown"])
+            self.vh_truth[3].path, "RewriteRule", ["UnknownPattern",
+                "UnknownTarget"])
         self.config.save()
 
         # This will create an ssl vhost for letsencrypt.demo
@@ -872,7 +873,7 @@ def test_redirect_with_existing_rewrite(self, mock_exe, _):
 
         self.assertEqual(len(rw_engine), 1)
         # three args to rw_rule + 1 arg for the pre existing rewrite
-        self.assertEqual(len(rw_rule), 4)
+        self.assertEqual(len(rw_rule), 5)
 
         self.assertTrue(rw_engine[0].startswith(self.vh_truth[3].path))
         self.assertTrue(rw_rule[0].startswith(self.vh_truth[3].path))
@@ -921,6 +922,42 @@ def test_create_own_redirect_for_old_apache_version(self):
         self.config._enable_redirect(self.vh_truth[1], "")  # pylint: disable=protected-access
         self.assertEqual(len(self.config.vhosts), 7)
 
+    def test_sift_line(self):
+        # pylint: disable=protected-access
+        small_quoted_target = "RewriteRule ^ \"http://\""
+        self.assertFalse(self.config._sift_line(small_quoted_target))
+
+        https_target = "RewriteRule ^ https://satoshi"
+        self.assertTrue(self.config._sift_line(https_target))
+
+        normal_target = "RewriteRule ^/(.*) http://www.a.com:1234/$1 [L,R]"
+        self.assertFalse(self.config._sift_line(normal_target))
+
+    def test_make_vhost_ssl_with_http_vhost_redirect_rewrite_rule(self):
+        self.config.parser.modules.add("rewrite_module")
+
+        http_vhost = self.vh_truth[0]
+
+        self.config.parser.add_dir(
+            http_vhost.path, "RewriteEngine", "on")
+
+        self.config.parser.add_dir(
+                http_vhost.path, "RewriteRule",
+                ["^",
+                "https://%{SERVER_NAME}%{REQUEST_URI}",
+                "[L,QSA,R=permanent]"])
+        self.config.save()
+
+        ssl_vhost = self.config.make_vhost_ssl(self.vh_truth[0])
+
+        #import ipdb; ipdb.set_trace()
+        self.assertTrue(self.config.parser.find_dir(
+            "RewriteEngine", "on", ssl_vhost.path, False))
+
+        conf_text = open(ssl_vhost.filep).read()
+        commented_rewrite_rule = \
+        "# RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [L,QSA,R=permanent]"
+        self.assertTrue(commented_rewrite_rule in conf_text)
 
     def get_achalls(self):
         """Return testing achallenges."""