mod_rewrite: Prevent multiple rewrites using an environment variable - apache

I'm currently returning a 404 error for *.php and internally redirecting all requests to a PHP file if one exists, using the following:
RewriteCond %{REQUEST_URI} /(?!index$)
RewriteCond %{REQUEST_FILENAME}\.php -f
RewriteRule ^(.+)$ /$1.php [QSA,L,E=norewrite:1]
RewriteCond %{ENV:norewrite} !1
RewriteCond %{REQUEST_URI} \.php$
RewriteRule ^(.+)$ - [R=404]
This works fine. However, I wish to have the ability to serve up PHP (or other) source (with the appropriate extension), from files e.g. index.php.src, while having index.php.src also return a 404 if accessed directly.
RewriteCond %{REQUEST_URI} /(?!index$)
RewriteCond %{REQUEST_FILENAME}\.php -f
RewriteRule ^(.+)$ /$1.php [QSA,L,E=norewrite:1]
RewriteCond %{ENV:norewrite} !1
RewriteCond %{REQUEST_FILENAME}\.src -f
RewriteRule ^(.+)$ /$1.src [L,E=norewrite:1]
RewriteCond %{ENV:norewrite} !1
RewriteCond %{REQUEST_URI} \.php$ [OR]
RewriteCond %{REQUEST_URI} \.src$
RewriteRule ^(.+)$ - [R=404]
This does not appear to work. It internally redirects to index.php, then to index.php.src, then 404s.
What's interesting is that, in the first sample, the environment variable DOES prevent the second ruleset from executing, and the page loads as expected. When I add that middle ruleset you see in the second sample, the environment variable no longer seems to have any effect.
If I remove that second ruleset from the second sample, leaving the additional lines in the last ruleset, as is, it behaves just like the first sample (except that requesting e.g. index.php.src returns a 404, which is what I want).
For various reasons, it would be unacceptable to use a query string for this purpose, it must be an environment variable.
How can I make this work? What am I doing wrong?
Edit:
In case I explained it poorly (I'm fairly sure I did)...
The following two files exist: 'index.php' and 'index.php.src'
If I request http ://domain.com/ with the first set of rules, I get my homepage (as expected). With the second set of rules, I get a 404. With the second set of rules, minus the second stanza, I get my homepage (as expected).
If I request http ://domain.com/index with either set of rules, I get a 404, as expected.
If I request http ://domain.com/index.php with either set of rules, I get a 404. This is expected with the first set, but I expect to be served the contents of 'index.php.src'.
If I request http ://domain.com/index.php.src with the first set of rules, I get the contents of 'index.php.src', as expected since the rule to 404 on *.src isn't in that set. I get a 404 as expected with the second set, with or without the second stanza.
The problem appears to be in the second stanza, but I can't make out what's wrong...

Here's what I did that worked:
RewriteCond %{IS_SUBREQ} false
RewriteCond %{REQUEST_URI} /(?!index$)
RewriteCond %{ENV:REDIRECT_STOP} !1
RewriteCond %{REQUEST_FILENAME}\.php -f
RewriteRule ^(.+)$ /$1.php [QSA,L,E=STOP:1]
RewriteCond %{IS_SUBREQ} false
RewriteCond %{ENV:REDIRECT_STOP} !1
RewriteCond %{REQUEST_FILENAME}\.src -f
RewriteRule ^(.+)$ /$1.src [L,E=STOP:1]
RewriteCond %{IS_SUBREQ} false
RewriteCond %{ENV:REDIRECT_STOP} !1
RewriteCond %{REQUEST_URI} \.php$ [OR]
RewriteCond %{REQUEST_URI} \.src$
RewriteRule ^(.+)$ - [R=404]
You'll notice I added the %{IS_SUBREQ} bits, which helped with the homepage redirerect issue that was causing it to 404. At that point, the query string method became acceptable, and I did get it working with that method, but I'm not the type to jsut let it be at that, I knew this could be done and I was gonna do it (I did!)
Aside from changing the variable name from 'norewrite' to 'STOP', which I did for clarity, I learned that environment variables set by mod_rewrite are prefixed with 'REDIRECT_' when an internal redirect occurs. That's why setting the value of 'norewrite' ('STOP'), then checking that same variable, was not working. When I appended 'REDIRECT_' to it in the check lines, it now behaves as expected.
Requesting '/awesome' will process 'awesome.php' and return its output
Requesting '/awesome.php' will return the content of 'awesome.php.src' (which is a symlink to 'awesome.php')
Requesting 'awesome.php.src' will return a 404
This is exactly what I wanted! Hopefully this will help someone else, as well.

Related

adding forward slash after rewrite if not exist

I have read a ton and tried a ton of solutions, but can't find one specific to my needs.
In my htaccess I have the following:
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^profile/([\w-]+)/?$ profile.php?username=$1 [L,QSA]
RewriteRule ^profile/([\w-]+)/([\w-]+)/?$ profile.php?username=$1&type=$2 [L,QSA]
RewriteRule ^profile/([\w-]+)/([\w-]+)/([\w-]+)/?$ profile.php?username=$1&type=$2&o=$3 [L,QSA]
This works wonderfully, except for 1 small problem.
If profile/username does not have a / the links on the page will break unless absolute urls are used. So <a href="./1"> will end up as profile/1 instead of profile/username/1
If I change the first rewrite to the following:
RewriteRule ^profile/([\w-]+)/$ profile.php?username=$1 [L,QSA]
then https://myurl/username will return a 404 which I do not want - I want it to force the / on the end if it does not exist.
I have tried adding:
RewriteRule ^(.*)([^/])$ /$1$2/ [L,R=301]
I have tried
RewriteRule ^(.*)$ https://nmyurl/$1/ [L,R=301]
Just can't figure out how to do this with the rewrite conditions already in place.
To add an optional traling at the end of your profile URLs you can use this
RewriteEngine on
RewriteCond %{REQUEST_URI} ^/profile/
RewriteRule !/$ %{REQUEST_URI}/ [L,R]
If profile/username does not have a / the links on the page will break unless absolute urls are used. So <a href="./1"> will end up as profile/1 instead of profile/username/1
If the issue only applies to URLs of the form /profile/<username> then I would be specific and only append the trailing slash to these specific URLs, otherwise, you are going to get a lot of redirects (which could be detrimental to SEO).
However, you should ensure that internal links are for the canonical URL (ie. with the trailing slash).
For example, the following should go before your existing rewrites:
RewriteRule ^(profile/[\w-]+)$ /$1/ [R=301,L]
Since the canonical URL requires a trailing slash this should be a 301 (permanent) redirect. (But test with a 302 first.)
Alternatively, instead of redirecting in .htaccess (if you are still linking to the slashless URL internally) then you could add a base element (that includes the trailing slash) to the head section to state what relative URLs should be relative to.
For example:
<base href="/profile/<username>/">
Aside:
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^profile/([\w-]+)/?$ profile.php?username=$1 [L,QSA]
RewriteRule ^profile/([\w-]+)/([\w-]+)/?$ profile.php?username=$1&type=$2 [L,QSA]
RewriteRule ^profile/([\w-]+)/([\w-]+)/([\w-]+)/?$ profile.php?username=$1&type=$2&o=$3 [L,QSA]
The two RewriteCond directives would seem to be entirely superfluous. RewriteCond directives only apply to the first RewriteRule that follows. But the RewriteRule patterns are unlikely to match real files anyway (unless you have files without extensions or directories with the same name).
So unfortunately I wasn't able to actually achieve what I was hoping to because there are multiple levels of variables that may or may not exist.
In part I used the solution provided by Amit:
RewriteCond %{REQUEST_URI} ^/profile/
RewriteRule !/$ %{REQUEST_URI}/ [L,R]
However this wasn't enough, because as pointed out by MrWhite there are 3 separate potential url's.
https://myurl/profile/username/
https://myurl/profile/username/type/
https://myurl/profile/username/type/o/
In this sitation username should always exist, but type and o may or may not exist.
So what I did was detect the level of the url and then created conditional . and .. using php.
The variable o is always numeric and variable type is never numeric so this worked for me.
if (isset($_GET['o'])) { $o = strip_tags($_GET['o']); }
elseif (isset($_GET['type']) && is_numeric($_GET['type'])) { $o = strip_tags($_GET['type']); }
Then I detect:
// if o is set or if type is numberic, use ..
if (isset($_GET['o']) || (isset($_GET['type']) && is_numeric($_GET['type']))) {
$dots = '..';
// if o is not set and type is not numeric just use .
} else {
$dots = '.';
}
end result of 1:
if url is https://myurl/profile/username/
result is https://myurl/profile/username/1/
if url is https://myurl/profile/username/3/
result is https://myurl/profile/username/1/
if url is https://myurl/profile/username/type/3/
result is https://myurl/profile/username/type/1/
Which was the desired outcome.

htaccess pretty urls not working

Folder structure:
- assets
- all css / js
- calsses
- all models, db ant etc
- views
- admin
- app
- index.php
- customers.php
.......
my .htaccess
RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{HTTP_HOST} ^(www.)?localhost:8080$
RewriteRule ^(.*)$ /views/$1
RewriteRule ^(/)?$ /views/index.php [L]
address : localhost:8080/app/ - working fine, but then I try to add pretty url for example in my customers.php - localhost:8080/app/customers.php?id=5 change to localhost:8080/app/customers/id/5
htaccess added new line:
RewriteRule /id/(.*) customers.php?id=$1
It's not working, it always return 500 Internal Server Error there could be the problem?
plus Need all urls without .php extend
You'd have to include those conditions for every rule. You'd be better off just rewriting everything to, say views/router.php then using PHP to include the different controllers, or serve a 404 when the URL isn't valid.
RewriteRule !^views/router\.php$ views/router.php [NS,L,DPI]
I agree with Walf in that handling routes through a router class is a better idea (especially in the long run!) than using .htaccess redirects.
However, as your question seems to be more about why is this not working than about how you should do it, here is an explanation for what is going on.
I will be using these URLs as examples:
localhost:8080
localhost:8080/app
localhost:8080/app/customers/id/5
Your first rule:
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{HTTP_HOST} ^(www.)?localhost:8080$
RewriteRule ^(.*)$ /views/$1
As you intended, this RewriteRule will match any URL which is not a file, not a directory, and made to localhost:8080.
localhost:8080 # not matched because it leads to a directory.
localhost:8080/app -> localhost:8080/views/app
localhost:8080/app/customers/id/5 -> localhost:8080/views/app/customers/id/5
Your next rule:
RewriteRule ^(/)?$ /views/index.php [L]
It is important to realize that RewriteCond statements apply only to the first RewriteRule following them, thus all that is being checked here is the path.
Side note: ^(/)?$, as you are not using $1, can be simplified to ^/?$.
localhost:8080 -> localhost:8080/views/index.php
localhost:8080/views/app # not matched
localhost:8080/views/app/customers/id/5 # not matched
As the L flag is specified, Apache will immediately stop the current iteration and start matching again from the top. The documentation is badly worded. Thus, localhost:8080/views/index.php will be run through the first rule, fail to match, be run through this rule, fail to match, and then as no other rules exist to check (yet) no rewrite will be done.
Now lets look at what happens when you add your broken rule.
RewriteRule /id/(.*) customers.php?id=$1
There are a few problems here. First, as you don't require that the URL start with /id/ the rule will always match a URL that contains /id/, even if you have already rewritten the URL. If you amended this by using ^/id/(.*), then you would still have issues as the string that the rewrite RegEx is tested against has leading slashes removed. Lastly and most importantly, customers.php does not exist in your root directory.
localhost:8080/views/index.php # not matched
localhost:8080/views/app # not matched
localhost:8080/views/app/customers/id/5 -> localhost:8080/customers.php?id=5
This is the last rule in your file currently, so now Apache will start over. customers.php does not exist in your directory, so it will be rewritten to views/customers.php. No other rules matched, but the URL has changed and so Apache will start over again, as /views/customers.php does not exist, it will be rewritten to /views/views/customers.php ... This pattern will repeat until you hit the maximum iteration limit and Apache responds with a 500 error.
You can solve this several ways. Here would be my preferred method, but only if you cannot use a router.
RewriteEngine on
# Rewrite the main page, even though it is a directory
RewriteRule ^/?$ views/index.php [END]
# Don't rewrite any existing files or directories
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule .? - [S=999,END]
RewriteRule ^app/?$ views/app/index.php [END]
RewriteRule ^app/id/(.*)$ views/app/customers.php?id=$1 [END]
TL;DR Use a PHP based router. .htaccess rules can be incredibly confusing.
Please refer to the question, How to make Clean URLs
I think this is what you needed.
you can use RewriteRule ^(.*)$ index.php [QSA,L]
Having another crack.
RewriteEngine on
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d [OR]
RewriteCond %{HTTP_HOST} !^(?:www\.)?localhost:8080$ [OR]
RewriteCond $0 =views
RewriteRule [^/]* - [END]
RewriteRule ^(app|admin)/([^/]+) views/$1/$2.php [DPI,END]
RewriteRule ^(app|admin)/?$ views/$1/index.php [DPI,END]
You may have to use L instead of END flags if your Apache is older. Set up an ErrorDocument for 404s, too.
Don't muck around with query strings, just parse $_SERVER['REQUEST_URI'] in PHP, e.g. start by exploding it on /. Then you'll have all the parameters of the original pretty URL. You can do that part in an include so each controller can reuse the same code.
I tried your structure and .htaccess file myself and found an endless loop in the apache logs. I bet you got something like this:
Mon Nov 28 19:57:32.527765 2016] [core:error] [pid 10] [client 172.18.0.1:35048] AH00124: Request exceeded the limit of 10 internal redirects due to probable configuration error. Use 'LimitInternalRecursion' to increase the limit if necessary. Use 'LogLevel debug' to get a backtrace.
I could fix it by adding the last rule like:
RewriteRule id/(.*) /views/app/customers.php?id=$1
The leading / is not needed for the match and the target needs the full path. Note that I got the id double (e.g. 123/123) on the url: http://localhost:8080/id/123.
This is caused by one of the 2 previous rules (removing them fixes it) so you might need to change them.
Here is what you want :
RewriteEngine On
RewriteBase /app/
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-l
RewriteRule ^\/?$ views/index.php [L]
RewriteRule ^([a-zA-Z0-9]+)\/([a-zA-Z0-9]+)\/([a-zA-Z0-9]+)\/?$ views/$1.php?$2=$3 [L]
RewriteRule ^([a-zA-Z0-9]+)\/?$ views/$1.php [L]

Why does mod_rewrite process the rules after the [L] flag

There are the rules:
RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule (.+) dir/index.php?$1 [L]
RewriteRule dir/index\.php.* - [F]
Why the last rule is processed and it returns Forbidden for all requests?
I need that if file or directory is not found then the next rule shouldn't be processed.
The next example isn't working for me as well:
RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule .? - [S=1]
RewriteRule dir/index\.php.* - [F]
RewriteRule (.+) dir/index.php?$1
It still returns Forbidden for all requests.
Why the last rule is processed and it returns Forbidden for all requests?
When the URL foobar is requested:
The two conditions (line 2, 3) match
Pattern matches, the resulting URL becomes dir/index.php?foobar (line 4)
The [L] flag causes the rewriting to stop -- it does not stop Apache from having another go at the rewritten URL since it has changed (see below).
With dir/index.php as the input URL:
The condition does not match (line 2) since file exists
Jumps to line 5
Pattern matches, hence the Forbidden error
When directory or filename changes, Apache has to re-evaluate various configuration sections (e.g. Directory and Files) and the .htaccess file for the "re-written" path. This is why Apache might perform another iteration even when the previous one was ended by [L] flag.
The last string supposes to restrict the direct access to UFL handler.
Direct access means requesting the file through a link like: domain.com/dir/index.php
I think adding another condition before line 5 should work:
RewriteCond %{THE_REQUEST} dir/index\.php\x20HTTP/\d\.\d$
RewriteRule . - [F]
The THE_REQUEST server variable contains the request sent by the browser without any rewriting applied. This could be useful to detect what page was originally requested by the browser.
THE_REQUEST
The full HTTP request line sent by the browser to the server (e.g.,
"GET /index.html HTTP/1.1"). This does not include any additional
headers sent by the browser. This value has not been unescaped
(decoded), unlike most other variables below.
I am not exactly sure of what you meant by "the next rule".
But if you don't want some rules to be executed when a non-existent file is requested, then using the following structure may help. (The following piece of code is copied from the Apache RewriteRule Flags Page)
# Is the request for a non-existent file?
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
# If so, skip these two RewriteRules
RewriteRule .? - [S=2]
RewriteRule (.*\.gif) images.php?$1
RewriteRule (.*\.html) docs.php?$1
And also using [R] for redirecting instead of [L] might help with the problem of returning Forbidden for all requests.

Ignore requests from internal redirects

RewriteRule ^resources/.+$ - [L]
RewriteRule .? index.php?t=$0 [QSA,L]
Would produce a 500 - Internal Server Error, because it would repeat again and again the same rule, due to internal redirected requests which are exactly treated as the first one. It would lead to an infinite chain of index.php?t=index.php&t=index.php&t=index.php&[...infinite more...]&t=test.php
But in my opinion this is not much better:
RewriteRule ^resources/.+$ - [L]
RewriteCond %{QUERY_STRING} !t=
RewriteCond %{REQUEST_URI} !^index\.php$
RewriteRule .? index.php?t=$0 [QSA,L]
Because now the user could input index.php?t=test.php as address, would pass the script and get the same content as if he had given test.php. I don't like that.
So how do I execute the first one without the issue of repeating internal redirects? Surely, a flag VL - Very Last would do the trick but sadly it does not exist.
First we have a look at all parameters given to the rules possibly indicating whether this is a chained request or not. This means, we either 1) need a variable changed in chained requests not relative to the changed URI or 2) the opposite, a variable which is relative to the changed URI and did not change (because we can compare it then against the others who did chage).
The problem is, they almost all update according to the applied RewriteRules.
IS_SUBREQ (1) and THE_REQUEST (2) are the only interesting variables but sadly internal redirects are not treated as subrequests, so IS_SUBREQ disappears. Only THE_REQUEST does not change and contains the real given path, so we have found our entry point.
With this in mind here is the annoying complex solution:
RewriteEngine On
# Set SCRIPT_URI and SUBREQ
# MUST be the first statements in the file
# SCRIPT_URI is the original browser-requested path
# SUBREQ is "true" if the original browser-requested path is not overriden yet
RewriteCond %{ENV:REQUEST_PARSED} !true
RewriteCond %{THE_REQUEST} ^\s*\w+\s+(http://[^\s/]+/|/?)([^\s\?]*)[\s\?$]
RewriteRule .? - [E=SCRIPT_URI:/%2,C]
RewriteRule .? - [E=REQUEST_PARSED:true]
RewriteCond %{ENV:SCRIPT_URI} ^(.*?)/\.($|/.*$)
RewriteRule .? - [E=SCRIPT_URI:%1%2,N]
RewriteCond %{ENV:SCRIPT_URI} ^(.*?)/[^/]+/\.\.($|/.*$)
RewriteRule .? - [E=SCRIPT_URI:%1%3,N]
RewriteCond %{ENV:SCRIPT_URI} ^(.*?)//\.\.($|/.*$)
RewriteRule .? - [E=SCRIPT_URI:%1/%2,N]
RewriteCond %{ENV:SCRIPT_URI}#%{REQUEST_URI} !^/*(.*)#/*\1$
RewriteRule .? - [E=SUBREQ:true]
# SCRIPT_URI and SUBREQ are set now. Actual content follows:
RewriteCond %{ENV:SUBREQ} !true
RewriteRule ^resources/.+$ - [L]
RewriteCond %{ENV:SUBREQ} !true
RewriteRule .? index.php?t=$0 [QSA,L]

Mod rewrite - redirect issue - local server is fine, live server isn't

Having a bit of a problem with the below:
RewriteEngine On
RewriteOptions Inherit
RewriteBase /
#Add trailing slash if not a directory or file, but not if it contains a dot
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_URI} !/$
RewriteCond %{REQUEST_URI} !\.
RewriteRule .*[^/]$ $0/ [L,R=301]
# Don't redirect these directories
RewriteRule ^/?_images/.+$ - [L]
RewriteRule ^/?_lib/.+$ - [L]
RewriteRule ^/?_scripts/.+$ - [L]
RewriteRule ^/?_temp/uploads/.+$ - [L]
RewriteRule ^/?_template/.+$ - [L]
# Redirect via router, but not these files
RewriteCond %{REQUEST_URI} !^(\/!favicon.ico|robots.txt|sitemap.xml).*$
RewriteCond %{REQUEST_URI} !^\/$
RewriteRule ^(.*)$ index.php?route=$1 [QSA,L]
The problem I'm having is the 'not redirecting directories' - basically files in these folders should still show, - this was working correctly, but I'm having a bit of trouble displaying something from the /_lib/images/ directory - for some reason I always get a 404 error thrown up (possibly because it goes via the router).
On closer examination by displaying the error number, it actually seems to be a 406 error (which I've not come across before) and the 404 probably results from not having a 406 error page.
The weird thing is, it works perfectly locally on XAMPP but not on the live server - also, if I rename _lib/images/ to, say, _lib/a/ it will work perfectly ... so the question is, am I completely missing something here? Even a simple 'hello world' results the same....
406 Not Acceptable The requested resource is only capable of generating content not acceptable according to the Accept headers sent in the request.
Sounds like you don't have the associations for your image file types set up on the server. Move an image to DOCROOT, and then try to display it. If it returns a 406, then you need to set up the correct file associations.