How to redirect URL with htaccess (RewriteRule) and prevent direct access - apache

using .htaccess I'd like to transparently redirect requests for folder "old" to folder "new", and in the same time prevent direct access to folder "new":
desired result:
http://example.com/old/... -> will display what's in "new" (no URL change in browser!)
http://example.com/new/... -> no access
this is my code in .htaccess (the 1st line is here because several domains share the same root folder):
RewriteCond %{HTTP_HOST} ^example\.com$ [NC]
RewriteRule ^old(.*)$ new$1 [L]
RewriteRule ^new(.*)$ - [F]
Well, what happens is that the 3d line triggers because of the substitution in the 2nd. I was convinced that the flag "L" would prevent this from happening (end of processing), but it seems that's not the case.
Do you have any suggestions what needs to be done (I tried to debug with rewrite log, but without success)?
I did some logging and found the following:
[rid#d0ac98/initial] (3) [per-dir C:/www/example/] add path info postfix: C:/www/example/new -> C:/www/example/new/
[rid#d0ac98/initial] (3) [per-dir C:/www/example/] strip per-dir prefix: C:/www/example/new/ -> new/
[rid#d0ac98/initial] (3) [per-dir C:/www/example/] applying pattern '^new(.*)$' to uri 'new/'
[rid#d0ac98/initial] (4) RewriteCond: input='localhost' pattern='^example\.com$' => not-matched
[rid#d0ac98/initial] (4) RewriteCond: input='localhost' pattern='^localhost$' => matched
[rid#d0ac98/initial] (2) [per-dir C:/www/example/] rewrite new/ -> old/
[rid#d0ac98/initial] (3) [per-dir C:/www/example/] add per-dir prefix: old/ -> C:/www/example/old/
[rid#d0ac98/initial] (2) [per-dir C:/www/example/] strip document_root prefix: C:/www/example/old/ -> /example/old/
[rid#d0ac98/initial] (1) [per-dir C:/www/example/] internal redirect with /example/old/ [INTERNAL REDIRECT]
[rid#d217a8/initial/redir#1] (3) [per-dir C:/www/example/] strip per-dir prefix: C:/www/example/old/ -> old/
[rid#d217a8/initial/redir#1] (3) [per-dir C:/www/example/] applying pattern '^new(.*)$' to uri 'old/'
[rid#d217a8/initial/redir#1] (3) [per-dir C:/www/example/] strip per-dir prefix: C:/www/example/old/ -> old/
[rid#d217a8/initial/redir#1] (3) [per-dir C:/www/example/] applying pattern '^old(.*)$' to uri 'old/'
[rid#d217a8/initial/redir#1] (2) forcing 'C:/www/example/old/' to be forbidden
This seems an internal redirect, which causes the "forbidden" result. Indeed, the documentation mentions it:
It is therefore important, if you are using RewriteRule directives in one of these contexts, that you take explicit steps to avoid rules looping, and not count solely on the [L] flag to terminate execution of a series of rules, as shown below. An alternative flag, [END], can be used to terminate not only the current round of rewrite processing but prevent any subsequent rewrite processing from occurring in per-directory (htaccess) context. This does not apply to new requests resulting from external redirects...
So I suppose that in my example the error was due to the fact that I used "L" flag instead the "END" flag?
I found an alternative solution (3rd line is inserted here):
RewriteCond %{HTTP_HOST} ^example\.com$ [NC]
RewriteRule ^old(.*)$ new$1 [L]
RewriteCond %{ENV:REDIRECT_STATUS} ^$
RewriteRule ^new(.*)$ - [F]
The 4th line will be executed only if there's no internal redirect.

You can use:
RewriteEngine On
# if directly requesting /new then block it
RewriteCond %{THE_REQUEST} \s/+new(/\S*)?\s [NC]
RewriteRule ^ - [F]
# forward /old/abc to /new/abc
RewriteCond %{HTTP_HOST} ^(www\.)?example\.com$ [NC]
RewriteRule ^old(/.*)?$ new$1 [L,NC]
We used THE_REQUEST in first rule. THE_REQUEST variable represents original request received by Apache from your browser and it doesn't get overwritten after execution of some rewrite rules. REQUEST_URI on the other hand changes its value after other rewrite rules.
For the same reason your rule RewriteRule ^new(.*)$ - [F] will even block your request from /old/ since first rule changes URI to /new/.

Related

Rewrite subdomain to subdirectory in Apache .htaccess file

Suppose I have a domain called example.com and I want to use rewrite rules in the .htaccess file of Apache to rewrite:
https://office.example.com/index.html
to
https://example.com/office/index.html.
How would I do that? I checked lots of answers here, and the solution seems to be something like this:
RewriteEngine on
RewriteCond %{HTTP_HOST} ^office.example.com$
RewriteRule ^(.*)$ https://example.com/office/$1 [L,NC,QSA]
This works when I test it here:
https://htaccess.madewithlove.be?share=b40ca72f-86d3-5452-a04b-ac9f24812c57
Regrettably, it does generate an error 500 on my server. I enabled logging and found that this seems to be a recursion problem:
AH00124: Request exceeded the limit of 10 internal redirects.
In the logs it seems to add office to office endlessly: /office/office/office/.... I have no idea why this is happening. The rewritten URL doesn't meet the rewrite condition, so why would it do this?
I have found a way to make it "work". If I add R=301 to the RewriteRule attributes it does a redirect, and works, but I would prefer if the original URL remained in the address bar.
Here's the log for the first 2 redirects:
init rewrite engine with requested uri /
applying pattern '^(.*)$' to uri '/'
applying pattern '^(.*)$' to uri '/'
applying pattern '^(.*)$' to uri '/'
pass through /
[perdir /var/www/vhosts/example.com/httpdocs/] strip per-dir prefix: /var/www/vhosts/example.com/httpdocs/ ->
[perdir /var/www/vhosts/example.com/httpdocs/] applying pattern '^(.*)$' to uri ''
[perdir /var/www/vhosts/example.com/httpdocs/] rewrite '' -> 'https://example.com/office/'
reduce https://example.com/office/ -> /office/
[perdir /var/www/vhosts/example.com/httpdocs/] internal redirect with /office/ [INTERNAL REDIRECT]
#1 init rewrite engine with requested uri /office/
#1 applying pattern '^(.*)$' to uri '/office/'
#1 applying pattern '^(.*)$' to uri '/office/'
#1 applying pattern '^(.*)$' to uri '/office/'
#1 pass through /office/
#1 [perdir /var/www/vhosts/example.com/httpdocs/] strip per-dir prefix: /var/www/vhosts/example.com/httpdocs/office/
#1 [perdir /var/www/vhosts/example.com/httpdocs/] applying pattern '^(.*)$' to uri 'office/'
#1 [perdir /var/www/vhosts/example.com/httpdocs/] rewrite 'office/' -> 'https://example.com/office/office/'
#1 reduce https://example.com/office/office/ -> /office/office/
#1 [perdir /var/www/vhosts/example.com/httpdocs/] internal redirect with /office/office/ [INTERNAL REDIRECT]
#2 init rewrite engine with requested uri /office/office/
#2 applying pattern '^(.*)$' to uri '/office/office/'
#2 applying pattern '^(.*)$' to uri '/office/office/'
#2 applying pattern '^(.*)$' to uri '/office/office/'
#2 pass through /office/office/
#2 [perdir /var/www/vhosts/example.com/httpdocs/] add path info postfix: /var/www/vhosts/example.com/httpdocs/office
#2 [perdir /var/www/vhosts/example.com/httpdocs/] strip per-dir prefix: /var/www/vhosts/example.com/httpdocs/office/
#2 [perdir /var/www/vhosts/example.com/httpdocs/] applying pattern '^(.*)$' to uri 'office/office/'
#2 [perdir /var/www/vhosts/example.com/httpdocs/] rewrite 'office/office/' -> 'https://example.com/office/office/off
#2 reduce https://example.com/office/office/office/ -> /office/office/office/
#2 [perdir /var/www/vhosts/example.com/httpdocs/] internal redirect with /office/office/office/ [INTERNAL REDIRECT]
RewriteCond %{HTTP_HOST} ^office.example.com$
RewriteRule ^(.*)$ https://example.com/office/$1 [L,NC,QSA]
Rather confusing, this should implicitly trigger an external 302 (temporary) redirect, not an internal rewrite - when specifying a different host in the substitution string to the one being requested. (Although in my experience, any absolute URL in the substitution string triggers an external redirect.)
If it does trigger an internal rewrite (as indicated by the logs) then the requested hostname does not change (since this is not a separate request) and you will indeed get a rewrite loop.
However, if "the subdomain is an alias of the main domain" and a rewrite is what's required, then there is no need to specify a hostname in the substitution string and you will indeed need to make additional checks to prevent an internal rewrite loop (500 error).
Try the following instead:
RewriteEngine on
RewriteCond %{HTTP_HOST} ^office\.example\.com [NC]
RewriteRule !^office office%{REQUEST_URI} [L]
...to exclude any requests (including rewritten requests) that already start /office.
No need for the NC and QSA flags.
Alternatively, to only target direct requests (not rewritten requests) you could check the REDIRECT_STATUS environment variable instead (which is empty on the initial request and set to "200", as in 200 OK, after the first successful rewrite).
For example:
RewriteCond %{HTTP_HOST} ^office\.example\.com [NC]
RewriteCond %{ENV:REDIRECT_STATUS} ^$
RewriteRule (.*) office/$1 [L]
This has the added "benefit" that you can potentially have a sub-subdirectory called /office as well. ie. /office/office.
UPDATE: A third version is to check against the REQUEST_URI server variable. However, I would not expect this to be any different from the first version above.
RewriteCond %{HTTP_HOST} ^office\.example\.com [NC]
RewriteCond %{REQUEST_URI} !^/office
RewriteRule ^ office%{REQUEST_URI} [L]
Sadly enough, both your suggestions gave the same error as before.
Two things to try...
Add a slash prefix on the substitution string. ie. /office%{REQUEST_URI} and /office/$1 respectively. This changes the substitution string into a URL-path, rather than a relative filesystem path. However, I wouldn't necessarily expect this to make any difference in this respect. (It would be required for an external redirect.)
Use the END flag instead of L on the RewriteRule directives - this is an Apache 2.4 addition that should halt all processing. The L flag "only" ends the current pass before restarting the rewriting process (hence the need for additional checks to prevent rewrite loops).
But now any other file (IMG, CSS) gives an 404.
The above rewrites everything, so it will naturally rewrite all static resources if they don't already start /office. (If they already start /office then they should already be excluded by the above rules.)
To exclude common resources, you could make an exception (an additional RewriteCond directive) to exclude specific file extensions. For example:
RewriteCond %{REQUEST_URI} !\.(css|js|png|jpg|gif)$
And/or add an additional RewriteCond directive to exclude requests that already map to physical files (although this is "marginally" more expensive). For example:
RewriteCond %{REQUEST_FILENAME} !-f
Summary:
RewriteEngine On
RewriteCond %{HTTP_HOST} ^office\.example\.com [NC]
RewriteCond %{ENV:REDIRECT_STATUS} ^$
RewriteCond %{REQUEST_URI} !\.(css|js|png|jpg|gif)$
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule (.*) office/$1 [END]

Why doesn't the RewriteCond prevent the RewriteRule from processing?

I've got the following in my .conf file used for cache busting js:
<Directory "/www/virtual/site/html/js/">
RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^\d+\/(.+)$ $1 [L]
</Directory>
The idea here is to rewrite site.com/js/123456/script.js to site.com/js/script.js
(The rewriting works just fine)
If I look at the rewrite log the part I'm not understanding is that it is applying the RewriteRule to the script after the interal redirect... Shouldn't the %{REQUEST_FILENAME} !-f prevent it from processing the RewriteRule since the file exists?
Rewrite Log
[rid#2b607f8f2dc8/initial] (3) [perdir /www/virtual/site2/html/js/] add path info postfix: /www/virtual/site2/html/js/1234 -> /www/virtual/site2/html/js/1234/script.js
[rid#2b607f8f2dc8/initial] (3) [perdir /www/virtual/site2/html/js/] strip per-dir prefix: /www/virtual/site2/html/js/1234/script.js -> 1234/script.js
[rid#2b607f8f2dc8/initial] (3) [perdir /www/virtual/site2/html/js/] applying pattern '^\d+\/(.+)$' to uri '1234/script.js'
[rid#2b607f8f2dc8/initial] (2) [perdir /www/virtual/site2/html/js/] rewrite '1234/script.js' -> 'script.js'
[rid#2b607f8f2dc8/initial] (3) [perdir /www/virtual/site2/html/js/] add per-dir prefix: script.js -> /www/virtual/site2/html/js/script.js
[rid#2b607f8f2dc8/initial] (2) [perdir /www/virtual/site2/html/js/] strip document_root prefix: /www/virtual/site2/html/js/script.js -> /js/script.js
[rid#2b607f8f2dc8/initial] (1) [perdir /www/virtual/site2/html/js/] internal redirect with /js/script.js [INTERNAL REDIRECT]
[rid#2b6073eb14a8/initial/redir#1] (3) [perdir /www/virtual/site2/html/js/] strip per-dir prefix: /www/virtual/site2/html/js/script.js -> script.js
[rid#2b6073eb14a8/initial/redir#1] (3) [perdir /www/virtual/site2/html/js/] applying pattern '^\d+\/(.+)$' to uri 'script.js'
[rid#2b6073eb14a8/initial/redir#1] (1) [perdir /www/virtual/site2/html/js/] pass through /www/virtual/site2/html/js/script.js
mod_rewrite (by design) keeps on applying rewrite-rules, till the url no longer changes.
So the first time it rewrite 1234/script.js to script.js the second time it tries to apply the rule, the pattern regular expression doesn't match and script.js is 'passed through'.
This is the way it is supposed to work. It can sometimes be a pain, but it makes mod_rewrite a lot more powerful/useful.
edit
mod_rewite first tests the RewriteRule pattern, before trying to apply the RewriteCond's. This is so you can use backreferences $0, $1 etc. in your RewriteCond. After all the RewriteCond's it will create the substitution string, so it can also use backrefereces %0, %1. etc. See also the diagram below.
So this is why it tried to apply the pattern before testing if the file exists.

Mod_rewrite: Ruleset causes strange split and append in rewrite log

What causes the querystring to be split out and what
causes /news/50 to be appended in the second run (as an effect of the next flag N).
The behaviour I'm after is to rewrite /about/news/50/ to /about/news/news-item?post_id=50.
I add the N flag since I want to send the rewritten uri /about/news/news-item?post_id=50 to index.php.
I have the following rules in my .htaccess file:
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule %{REQUEST_FILENAME} !-f
RewriteRule %{REQUEST_FILENAME} !-d
RewriteRule ^about/news/(\d+)/$ /about/news/news-item?post_id=$1 [QSA,N]
RewriteRule ^index\.php$ - [QSA,L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [QSA,L]
</IfModule>
The log entries:
(2) init rewrite engine with requested uri /about/news/50/
(1) pass through /about/news/50/
(3) add path info postfix: /wordpress/about -> /wordpress/about/news/50/
(3) strip per-dir prefix: /wordpress/about/news/50/ -> about/news/50/
(3) applying pattern '%{REQUEST_FILENAME}' to uri 'about/news/50/'
(3) add path info postfix: /wordpress/about -> /wordpress/about/news/50/
(3) strip per-dir prefix: /wordpress/about/news/50/ -> about/news/50/
(3) applying pattern '%{REQUEST_FILENAME}' to uri 'about/news/50/'
(3) add path info postfix: /wordpress/about -> /wordpress/about/news/50/
(3) strip per-dir prefix: /wordpress/about/news/50/ -> about/news/50/
(3) applying pattern '^about/news/(\d+)/$' to uri 'about/news/50/'
(2) rewrite 'about/news/50/' -> '/about/news/news-item?post_id=50'
(3) split uri=/about/news/news-item?post_id=50 -> uri=/about/news/news-item, args=post_id=50
(3) add path info postfix: /about/news/news-item -> /about/news/news-item/news/50/
(3) applying pattern '%{REQUEST_FILENAME}' to uri '/about/news/news-item/news/50/'
(3) add path info postfix: /about/news/news-item -> /about/news/news-item/news/50/
(3) applying pattern '%{REQUEST_FILENAME}' to uri '/about/news/news-item/news/50/'
(3) add path info postfix: /about/news/news-item -> /about/news/news-item/news/50/
(3) applying pattern '^about/news/(\d+)/$' to uri '/about/news/news-item/news/50/'
(3) add path info postfix: /about/news/news-item -> /about/news/news-item/news/50/
(3) applying pattern '^index\.php$' to uri '/about/news/news-item/news/50/'
(3) add path info postfix: /about/news/news-item -> /about/news/news-item/news/50/
(3) applying pattern '.' to uri '/about/news/news-item/news/50/'
(4) RewriteCond: input='/about/news/news-item' pattern='!-f' => matched
(4) RewriteCond: input='/about/news/news-item' pattern='!-d' => matched
(2) rewrite '/about/news/news-item/news/50/' -> '/index.php'
(2) trying to replace prefix /wordpress/ with /
(1) internal redirect with /index.php [INTERNAL REDIRECT]
(2) init rewrite engine with requested uri /index.php
(1) pass through /index.php
(3) strip per-dir prefix: /wordpress/index.php -> index.php
(3) applying pattern '%{REQUEST_FILENAME}' to uri 'index.php'
(3) strip per-dir prefix: /wordpress/index.php -> index.php
(3) applying pattern '%{REQUEST_FILENAME}' to uri 'index.php'
(3) strip per-dir prefix: /wordpress/index.php -> index.php
(3) applying pattern '^about/news/(\d+)/$' to uri 'index.php'
(3) strip per-dir prefix: /wordpress/index.php -> index.php
(3) applying pattern '^index\.php$' to uri 'index.php'
(1) pass through /wordpress/index.php
Try the DPI (Discard PATH_INFO flag) if you rewrite a URL that had PATH_INFO stripped from it and don't want it added back on. It's a problem with per-directory rewrites
edit: if you do this, since wordpress counts on the PATH_INFO, you'd have to actually rewrite to /index.php/about/news/... to let the next round still see PATH_INFO.
RewriteRule ^about/news/(\d+)/$ /index.php/about/news/news-item?post_id=$1 [DPI,L]
instead of
RewriteRule ^about/news/(\d+)/$ index.php [DPI,L]

Help understanding rewrite log (want to internally rewrite a page when requested from specific HTTP_HOST)

I have a Drupal site, site.com, and our client has a campaign that they're promoting for which they've bought a new domain name, campaign.com. I'd like it so that a request for campaign.com internally rewrites to a particular page of the Drupal site. Note Drupal uses an .htaccess file in the document root.
The normal Drupal rewrite is
# Rewrite URLs of the form 'x' to the form 'index.php?q=x'.
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} !=/favicon.ico
RewriteRule ^(.*)$ index.php?q=$1 [L,QSA]
I added the following before the normal rewrite.
# Custom URLS (eg. microsites) go here
RewriteCond %{HTTP_HOST} =campaign.com
RewriteCond %{REQUEST_URI} =/
RewriteRule ^ index.php?q=node/22 [L]
Unfortunately it doesn't work, it just shows the homepage. Turning on the rewrite log I get this.
1. [rid#2da8ea8/initial] (3) [perdir D:/wamp/www/] strip per-dir prefix: D:/wamp/www/ ->
2. [rid#2da8ea8/initial] (3) [perdir D:/wamp/www/] applying pattern '^' to uri ''
3. [rid#2da8ea8/initial] (2) [perdir D:/wamp/www/] rewrite '' -> 'index.php?q=node/22'
4. [rid#2da8ea8/initial] (3) split uri=index.php?q=node/22 -> uri=index.php, args=q=node/22
5. [rid#2da8ea8/initial] (3) [perdir D:/wamp/www/] add per-dir prefix: index.php -> D:/wamp/www/index.php
6. [rid#2da8ea8/initial] (2) [perdir D:/wamp/www/] strip document_root prefix: D:/wamp/www/index.php -> /index.php
7. [rid#2da8ea8/initial] (1) [perdir D:/wamp/www/] internal redirect with /index.php [INTERNAL REDIRECT]
8. [rid#2da7770/initial/redir#1] (3) [perdir D:/wamp/www/] strip per-dir prefix: D:/wamp/www/index.php -> index.php
9. [rid#2da7770/initial/redir#1] (3) [perdir D:/wamp/www/] applying pattern '^' to uri 'index.php'
10.[rid#2da7770/initial/redir#1] (3) [perdir D:/wamp/www/] strip per-dir prefix: D:/wamp/www/index.php -> index.php
11.[rid#2da7770/initial/redir#1] (3) [perdir D:/wamp/www/] applying pattern '^(.*)$' to uri 'index.php'
12.[rid#2da7770/initial/redir#1] (1) [perdir D:/wamp/www/] pass through D:/wamp/www/index.php
I'm not used to mod_rewrite, so I might be missing something, but comparing the logs from a call to http://site.com/node/3 and from http://campaign.com/ I can't see any meaningful difference. Specifically uri and args on line 4 seem correct, the internal redirect on line 7 seems right, and the pass through on line 12 seems right (because the file index.php exists). But for some reason it seems the query string's been discarded/ignored around the time of the internal redirect. I'm completely stumped.
Also, if anyone could provide a reference on understanding the rewrite log, that might help. It'd be great if there's a way to track the query string through the internal redirect.
FWIW I'm using WampServer 2.1 with Apache 2.2.17.
Thanks for asking this question, it's something that I need to do too. I don't know the way to do this by means of the .htaccess, and hope that someone here can answer that.
But I do the same thing by using Drupal's menu system with this code in a custom module:
function mymodule_menu() {
$items = array();
$items['domain_redirect'] = array(
'page callback' => 'domain_redirect',
'type' => MENU_NORMAL_ITEM,
'access arguments' => array('access content'),
);
return $items;
}
function domain_redirect() {
switch ($_SERVER['SERVER_NAME'])
{
case "campaign.com":
$goto = "node/22";
break;
default:
$goto = "/";
}
drupal_goto($goto);
}
Then set the frontpage to domain_redirect.

A mod_rewrite problem

I'm trying to implement the following mod_rewrite rule:
host.com/developer/ => host.com/developer/index.py
host.com/developer/branchX => host.com/developer/index.py?branch=branchX
host.com/developer/branchX/commitY => host.com/developer/index.py?branch=branchX&commit=commitY
Currently, the appropriate config section looks like this:
Options FollowSymLinks
AllowOverride None
RewriteEngine on
RewriteRule ^([^/]+)$ /$1/index.py [L]
RewriteRule ^([^/]+)/([^/]+) /$1/index.py?branch=$2 [L]
RewriteRule ^([^/]+)/([^/]+)/([^/]+)$ /$1/index.py?branch=$2&commit=$3 [L]
However, after the URL has been initially rewritten, an internal redirect occurs and the URL is being rewritten again, ruining it. The process repeats many times and eventually results into a 500 error. The log (timestamps and perdir parts removed):
[..initial] (3) strip per-dir prefix: /home/www/host.com/master/a -> master/a
[..initial] (3) applying pattern '^([^/]+)$' to uri 'master/a'
[..initial] (3) strip per-dir prefix: /home/www/host.com/master/a -> master/a
[..initial] (3) applying pattern '^([^/]+)/([^/]+)' to uri 'master/a'
[..initial] (2) rewrite 'master/a' -> '/master/index.py?branch=a'
[..initial] (3) split uri=/master/index.py?branch=a -> uri=/master/index.py, args=branch=a
[..initial] (1) internal redirect with /master/index.py [INTERNAL REDIRECT]
[..initial/redir#1] (3) strip per-dir prefix: /home/www/host.com/master/index.py -> master/index.py
[..initial/redir#1] (3) applying pattern '^([^/]+)$' to uri 'master/index.py'
[..initial/redir#1] (3) strip per-dir prefix: /home/www/host.com/master/index.py -> master/index.py
[..initial/redir#1] (3) applying pattern '^([^/]+)/([^/]+)' to uri 'master/index.py'
[..initial/redir#1] (2) rewrite 'master/index.py' -> '/master/index.py?branch=index.py'
[..initial/redir#1] (3) split uri=/master/index.py?branch=index.py -> uri=/master/index.py, args=branch=index.py
How can I fix my rules to prevent the endless internal redirects?
Thanks.
The problem is that the url you are rewriting to is matched in the next pass. L specifies the last rule but only for this execution of the rules, URLs are processed again under certain circumstances (in this case, internal redirects). The solution is to add a RewriteCond to prevent looping, like this:
RewriteEngine on
RewriteCond %{REQUEST_URI} !index\.py
RewriteRule ^([^/]+)$ /$1/index.py [L]
RewriteCond %{REQUEST_URI} !index\.py
RewriteRule ^([^/]+)/([^/]+) /$1/index.py?branch=$2 [L]
RewriteCond %{REQUEST_URI} !index\.py
RewriteRule ^([^/]+)/([^/]+)/([^/]+)$ /$1/index.py?branch=$2&commit=$3 [L]