Whitelist in .htaccess
Instead of blacklisting inaccessible directories (like with deny all
) I want to use a whitelist. Basically, I need this functionality:
- If the uri requests a file that exists in /public directory, display it;
- Otherwise route the request to /public/index.php;
- 'public' string is not needed in request string:
http://site.com/flower.jpg
displaysDOCUMENT_ROOT/public/flower.jpg
file from the file system;
Example:
Directory structure:
public\
flower.jpg
index.php
data\
secret_file.crt
Request string and expected result:
site.com/flower.jpg
- flower.jpg is displayed
site.com/data/secret_file.crt
site.com/public/flower.jpg
site.com/public
site.com/data
site.com/any/random_url
- request is routed to public/index.php
What I have now:
(and even that with outside help)
# the functionality described in #1 above
RewriteCond %{DOCUMENT_ROOT}/public%{REQUEST_URI} -f
RewriteRule .* public%{REQUEST_URI} [L]
# I'd like to take out the following line so ALL other 开发者_如何学编程requests route to index.php
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule .* public/index.php
If I remove the
RewriteCond %{REQUEST_FILENAME} !-f
line, it seizes to work, I've experimented countless configurations, read the modRewrite
docs but can't figure out why this simple thing refuses to simply function.
Can anyone help me out or point in the right direction?
Complete final solution for reference
RewriteEngine On
# following line stops mod_rewrite from looping because this rule has already been applied
RewriteCond %{REQUEST_URI} !^/public/index.php
RewriteCond %{DOCUMENT_ROOT}/public%{REQUEST_URI} -f
RewriteRule .* /public%{REQUEST_URI} [L]
# don't apply this rule if the first rule has been applied
RewriteCond %{REQUEST_URI} !^/public/
RewriteRule .* /public/index.php [L]
It's a little more complicated when the application is in a subdirectory, like http://site.com/uk/
, but this works great.
Ok, this is going to be a little confusing to explain. The problem you are having is that when mod_rewrite rewrites something, without the [R] or [P], it redirects internally, and all the rewrite rules get applied again. This keeps happening until the rewritten uri is the same as the un-rewritten uri. So the first rule you have is getting rewritten by the second rule. You need to prevent that from happening.
First, let's look at the first rule. What you had is totally fine, except you need to add a condition for the caveat site.com/public/flower.jpg rerouted to public/index.php
. This means if the request itself has a /public/
in it, it will not serve the request (and let the 2nd rule handle things). An additional caveat here is if you have a directory "public" inside "/public", as in DOCUMENT_ROOT/public/public/
, it will be inaccessible.
# Make sure the request itself isn't for /public/
RewriteCond %{THE_REQUEST} !^[A-Z]+\ /public/
# Make sure the filename exists.
RewriteCond %{DOCUMENT_ROOT}/public%{REQUEST_URI} -f
RewriteRule ^ /public%{REQUEST_URI} [L]
Here we've done the extra check for a request starting with something like GET /public/flower.jpg
, if it matches, we skip this rule entirely. Also, this rule will break if you try to access a directory in /public/
. For example, if you have a directory "stuff" inside "/public" and try to access it via the request site.com/stuff/
, this rule will not allow you to see the contents (even if there is an index.html file in /stuff/) because you are not checking if directories exist. You can do that by adding this condition for -d, like so:
# Make sure the request itself isn't for /public/
RewriteCond %{THE_REQUEST} !^[A-Z]+\ /public/
# Make sure the filename/directory exists.
RewriteCond %{DOCUMENT_ROOT}/public%{REQUEST_URI} -f [OR]
RewriteCond %{DOCUMENT_ROOT}/public%{REQUEST_URI} -d
RewriteRule ^ /public%{REQUEST_URI} [L]
The -d condition along with the [OR] of the -f says: if %{DOCUMENT_ROOT}/public%{REQUEST_URI}
is a regular file OR a directory. (See the RewriteCond docs)
Now for the second rule, and this is going to look a bit confusing because we have to handle the negation of the first rule's conditions. If the first rule passes and the URI is rewritten, 2 things happen:
- The request doesn't start with something like: GET /public/
- The uri got rewritten to "/public/[something]"
So we'll have 2 conditions to deal with that. If the first rule rewrote the URI, we don't want to touch it again. This solves the problem that I mentioned in the first paragraph. Additionally, we don't want to URI to get re-rewritten, causing a rewrite loop. So we need to add a condition to stop rewriting if the 2nd rule has already been applied, which means the URI is now /public/index.php
. Here are the combination of those conditions:
# stops mod_rewrite from looping because this rule has already been applied
RewriteCond %{REQUEST_URI} !^/public/index.php
# don't apply this rule if the first rule has been applied
RewriteCond %{THE_REQUEST} ^[A-Z]+\ /public/ [OR]
RewriteCond %{REQUEST_URI} !^/public/
RewriteRule ^ /public/index.php [L]
This may work:
RewriteCond %{DOCUMENT_ROOT}/public%{REQUEST_FILENAME} -f [OR]
RewriteCond %{DOCUMENT_ROOT}/%{REQUEST_FILENAME} -f [OR]
RewriteCond %{DOCUMENT_ROOT}%{REQUEST_FILENAME} -f
RewriteRule (.*) public$1 [QSA,L]
RewriteRule .* public/index.php
The optimized version may work too but I'm not sure:
RewriteCond %{DOCUMENT_ROOT}(/public|public|)%{REQUEST_FILENAME} -f
RewriteRule (.*) public$1 [QSA,L]
RewriteRule .* public/index.php
By the way your logic is weird: the following rule:
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule .* public/index.php
Means: "if the request is not a file, rewrite to public/index.php
". The problem is here: if it's a file, what's going on? Nothing. The RewriteRule
is ignored. This is not safe, imagine if it's a file that you may not want the user to access? Just remove this rule, it's useless, and without it, it's safer (from my point of view).
May I ask you to tell me if the optimized version worked?
Please try to use the RewriteLog
directive: it helps you to track down such problems:
# Trace:
# (!) file gets big quickly, remove in prod environments:
RewriteLog "/web/logs/mywebsite.rewrite.log"
RewriteLogLevel 9
RewriteEngine On
Tell me if it works.
I'm a bit confused with your first set of rules, since %{REQUEST_URI} would be /public/flower.jpg if I'm not mistaking. I would have done it this way :
RewriteCond public/%{REQUEST_FILENAME} -f
RewriteRule ^.*$ public/%{REQUEST_FILENAME} [L]
RewriteCond public/%{REQUEST_FILENAME} !-f
RewriteRule ^.*$ public/index.php [L]
I'm not sure of the behaviour if %{REQUEST_FILENAME} is empty but basically the rules says:
If the filename exists in public, rewrite all URI to that file, if it does not rewrite to index.php
Would that work for you?
Have you considered programmatically creating your .htaccess file to blacklist anything that isn't on a whitelist that you set in whatever file you use to create it? If you ask me, you can't get much simpler.
精彩评论