2

So I'm having this crazy-making issue happen on apache 2.4/CentOS7 with a web app.

I want to redirect all non-secure URLs on the app domain to secure versions, EXCEPT for one URI (a webhook endpoint for a vendor sandbox mode - no valid SSL cert as this is not production so we must allow HTTP). In addition to the app domain, there are customer site domains that also run through this, so we DO NOT want those to redirect since none have SSL so we explicitly match the HTTP_HOST.

This is a front controller pattern, we want index.php to handle all these requests to non-files and non-directories for all domains and over SSL for only the app domain.

It SEEMS so easy:

RewriteEngine on
RewriteBase "/"

# Redirect all app domain urls to ssl
RewriteCond %{HTTPS} off
RewriteCond %{HTTP_HOST} "^app\.example\.com$" [NC]
RewriteCond %{REQUEST_URI} "!^/AllowThisUri/Http"
  RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]

# I had to split this -d rule off of the -f rule to account for custom
# domain homepages from not being routed to the front controller when
# transitioning from centos6/apache2.2 for some reason
RewriteCond %{REQUEST_FILENAME} -d
RewriteCond %{REQUEST_URI} "!^/$"
  RewriteRule "^(.*)$" index.php [L]

# Redirect non-files to entry script
RewriteCond %{REQUEST_FILENAME} !-f
  RewriteRule "^(.*)$" index.php [L]

But what is happening is when I test with

curl http://app.example.com/AllowThisUri/Http

The last rule to pass to index.php is being used but instead of it actually being processed by index.php, it is redirecting there... curl returns (shortened response)

<title>301 Moved Permanently</title>
<p>The document has moved <a href="https://app.example.com/index.php">here</a>.</p>

So if I comment out this new exclusion RewriteCond:

#RewriteCond %{REQUEST_URI} "!^/AllowThisUri/Http"

The expected thing happens, it redirects to the same URL except with https:

<title>301 Moved Permanently</title>
<p>The document has moved <a href="https://app.example.com/AllowThisUri/Http">here</a>.</p>

If I completely remove the redirect to HTTPS logic, I do get the response from the front controller I expect (it just prints "hi" for testing).

#RewriteCond %{HTTPS} off
#RewriteCond %{HTTP_HOST} "^app\.example\.com$" [NC]
#RewriteCond %{REQUEST_URI} "!^/AllowThisUri/Http"
  #RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]

So I put it all back in - exactly as we started, and I can modify the R=301 to 302 like so:

RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=302,L]

and sure enough it changes the bad unexpected redirect response:

<title>302 Found</title>
<p>The document has moved <a href="https://app.example.com/index.php">here</a>.</p>

I hope this makes sense. It is long-winded to explain, I apologize.

What I see is even though the !^/AllowThisUri/Http condition is matching and we skip the HTTPS redirect rule, and the rules continue to process, by the time it gets to the last rule sending it to index.php, it's like the redirect part of the RULE IT SKIPPED seems to be hanging around somehow and affecting the result. It makes absolutely no sense to me.

To reiterate, without excluding the one URI with a RewriteCond, non-ssl to ssl redirects work as expected. To completely remove all this redirect logic, the index.php front controller responds with the expected response. It is when we add this one additional RewriteCond to exclude the one URI, that it behaves in the incorrect/unexpected way and returns a redirect TO index.php instead of it being processed and returning the expected response from the front controller.

Maybe someone knows of some obscure settings or maybe an apache 2.4 thing that would cause this?

0

The "problem" is that the rewriting process does not end when it gets to the last directive in a directory context (.htaccess?). The rewriting process starts over and does so repeatedly until the URL passes through unchanged, or a redirect occurred.

So, what is happening when you request http://app.example.com/AllowThisUri/Http is:

  1. The initial redirect is indeed being skipped because of the condition that explicitly excludes that one URL.
  2. The request is internally rewritten to index.php by the last rule.

The rewriting process now starts over, with the rewritten URL (ie. http://app.example.com/index.php):

  1. http://app.example.com/index.php is "redirected" to https://app.example.com/index.php by the first rule. Because of the L flag, the current round of processing stops (ie. the remaining 2 rewrite rules are skipped).

  2. Because a redirect has been triggered (3xx response code), the rewriting process does not start over and the redirect response is sent.

Solution

One solution is to only trigger the redirect on direct requests from the client, not rewritten requests (to index.php). To do this we can add an additional condition to the redirect that checks the REDIRECT_STATUS environment variable. This variable is empty initially and set to "200" after the first successful rewrite.

For example:

# Redirect all app domain urls to ssl
RewriteCond %{ENV:REDIRECT_STATUS} ^$
RewriteCond %{HTTPS} off
RewriteCond %{HTTP_HOST} "^app\.example\.com$" [NC]
RewriteCond %{REQUEST_URI} "!^/AllowThisUri/Http"
RewriteRule .* https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]

(Small tidy-up... no need to capture the RewriteRule pattern if the backreference is not being used.)

If there is only 1 URL you want to exclude then it is more efficient to check this in the RewriteRule pattern, instead of using a condition and checking against the REQUEST_URI server variable (unless this same URL is also used for other hostnames?). For example:

# Redirect all app domain urls to ssl
RewriteCond %{ENV:REDIRECT_STATUS} ^$
RewriteCond %{HTTPS} off
RewriteCond %{HTTP_HOST} ^app\.example\.com$ [NC]
RewriteRule !^AllowThisUri/Http https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.