Clean-URL rewrite rule with nested "location" and alias directive

Ben Johnson ben at indietorrent.org
Thu Nov 21 03:31:07 UTC 2013



On 11/20/2013 4:10 AM, Francis Daly wrote:

>> I think that you're exactly right. I had tried try_files first, but was
>> unable to get it to work given that this site a) must be accessed via a
>> "subdirectory" relative to the domain-root URL, and b) is comprised of
>> files that live in a "private" directory that is outside of the
>> server-root on the filesystem.
> 
> Generally it shouldn't be a problem -- file space and url space are
> different, and one of the main points of web server configuration is to
> map between them.
> 
> If you do have free choice in the matter, some things work more easily
> within nginx if you can use "root" and not "alias" -- so if you want
> files to be accessible below the url /stage/, having them directly in
> a directory called /stage/ is convenient.
> 
> (So if you can either get rid of the web/ directory and move all contents
> up one, or add a stage/ below web/ and move all contents down one,
> then set "root" appropriately and the remaining configuration may become
> simpler.)
> 

Ultimately, I have control of the configuration, so I could go either route.

But in the first case, my hesitation is two-fold:

a.) I prefer to maintain identical directory structures within my
staging and production environments. This allows my build scripts and
relative paths to remain identical (provided that I set a "root path"
variable or similar in each shell script), and it helps me to remember
to where I must "cd" in order to execute a build script.

b.) I really prefer to keep PHP and PHP-template files out of the
document root (on the filesystem). I don't want user-agents to be able
to request .tpl files directly, for example, by requesting
/templates/home.tpl. The same applies to configuration files, e.g.,
/config.inc.php. Most folks have seen what happens when a
misconfiguration allows a "sensitive configuration file" to be
downloaded as a plaintext file. For these reasons, and as a matter of
course, I build my applications such that every request is routed
through a single point that has complete authority over how the request
is handled.

In the second case, this seems risky; if I were to put the "stage"
directory within the production site's document root, it's conceivable
that doing so could have unintended consequences, such as the staging
site being deleted inadvertently during the build process (think rsync
with the --delete flag, as an
off-the-cuff-though-not-something-I-would-actually-do-myself example of
a crude "clean build process"). Issue b.) from above applies here, too.

>>> ===
>>>       location ^~ /stage/ {
>>>         alias /var/www/example.com/private/stage/web/;
>>>         index index.php index.html index.htm;
>>>         try_files $uri $uri/ /stage//stage/index.php?q=$uri;
>>>
>>>         location ~ ^/stage/(.+\.php)$ {
>>>                 alias /var/www/example.com/private/stage/web/$1;
>>>                 try_files "" / /stage/index.php?q=$uri;
>>>                 fastcgi_pass unix:/var/run/php5-fpm.sock;
>>>                 fastcgi_param HTTPS on;
>>>                 fastcgi_param SCRIPT_FILENAME $request_filename;
>>>                 include /etc/nginx/fastcgi_params;
>>>         }
>>>       }
>>> ===
> 
>> had read at http://wiki.nginx.org/HttpCoreModule#alias : "Note that
>> there is a longstanding bug that alias and try_files don't work
>> together" (with link to http://trac.nginx.org/nginx/ticket/97 ).
> 
>> Is the implication here that "alias" does indeed work with "try_files"
>> (even in my stale nginx-1.1.19 version)? At least in this particular
>> use-case?
> 
> I'd say rather that this configuration works with the current
> implementation of the defect.
> 
> So if the defect is fixed, this configuration will start to fail, but a
> more obvious working configuration will be available; but if the defect
> is changed or partly fixed, this configuration may start to fail without
> an equivalent workaround.
> 
> The ticket-#97 page currently (last modified date 2013-08-23) lists
> three aspects in the description.
> 
> The second aspect is, as I understand it, that in a prefix location with
> alias, if the fallback contains a $variable and begins with the location
> prefix, then the location prefix is stripped before the internal rewrite
> is done. In this configuration, that's why the extra /stage/ is in the
> first try_files.
> 
> The third aspect is, as I understand it, that in a regex location
> with alias, $document_root is set to the alias, which in this case is
> equivalent to the wanted filename. That's why an argument of "" finds
> the file, if it is present, in the second try_files.
> 
> I strongly suspect that the second argument there, /, can safely be
> dropped -- it can only apply if there is a directory called something.php;
> and in that case, I see no difference with the / there or not (in 1.5.7).
> 

Your ability to digest exactly what's happening here is inspiring. And
humbling. :) I'm glad that your explanation is now on-record (for my own
reference, if no one else's).

>> There is one last trick to pull-off, which is to add very similar
>> clean-URL functionality for two other files (in addition to index.php),
>> but I am hoping that I will be able to adapt your working sample myself.
> 
> I don't fully follow what you mean there; but once you can isolate your
> requests in a location, then you should be able to set the fallback to
> whatever you want.
> 

Oops, I misspoke!

I meant to say that there are certain cases in which I want index.php to
dispatch the request to dedicated CSS and JS controllers. So, rather
than request /css.php?file=home.css in the HTML, for example, I would
request /css2/home.css (the /css2/ is to distinguish "virtual" requests
from "real" requests for un-processed files in the standard /css/
directory). In these cases, index.php looks for /css2/ and dispatches
the request to the CSS controller, which in-turn obtains the requested
file from the URL (everything after /css2/) and returns the output when
the template file exists and is valid.

Maybe it would be helpful for me to demonstrate how I achieve this when
the "site" is not accessed in a subdirectory (with respect to the URL)
and the files actually exist in the virtual host's document root. (This
is counter to the scenario that we've thus far addressed in this
thread.) You actually helped me arrive at this solution a few months back.

###############################################################

location / {
	try_files $uri $uri/ @virtual;
}

location @virtual {
	include fastcgi_params;
	fastcgi_pass unix:/var/lib/php5-fpm/web1.sock;
	fastcgi_index index.php;
	fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
	fastcgi_intercept_errors on;
	
	if ($uri ~ '/download/.*/$') {
		rewrite ^/(.*)/$ /$1 permanent;
	}
	
	if ($uri !~ '(/|\.[a-zA-Z0-9]{1,12}|/download/.*)$') {
		return 301 $uri/$is_args$args;
	}
	
	if ($uri ~ '(/|/download/.*|/css2/.*|/js2/.*)$') {
		rewrite  ^(.*)$  /index.php?q=$1  last;
	}
}

###############################################################

This is all I'm trying to do. I just want to replicate the above with
the "less-friendly" directory structure that we've been discussing.

Regarding the rewrite statements (in order of appearance):

1.) If the URI is requesting a download and a trailing slash was
included, remove the trailing slash.

2.) If the URI doesn't end with a trailing slash, a file extension, or
contain "/download/...", redirect and add a trailing slash.

3.) If the URI ends with a trailing slash or contains "/download/...",
pass the request to PHP controller. Note that URLs ending in a file
extension with a trailing slash will be passed to the PHP controller, too.

If there is a better, more concise, or more secure way to accomplish the
above, then I am eager to learn.

> It may get to the point that it is clearer to use a @named location as
> the fallback, and just hardcode SCRIPT_FILENAME in them. You'll see when
> you do it, no doubt.
> 

Hmm, I think I see what you're getting at here. But is there no means by
which to achieve the desired configuration with my particular directory
structure? I'm a bit surprised at how difficult it is to decouple the
request location from the filesystem directory from which it is served,
while maintaining this clean-URL setup.

>>> You may want to use something other than $uri in the last argument to
>>> try_files, depending on how you want /stage/my-account/?key=value to
>>> be processed.
> 
> The usual caveats apply here regarding url escaping -- you've copied
> an unescaped string directly into query string, so if there were any
> characters like % or + or & involved, they may cause confusion.
> 

Understood. The snippet that I pasted above does everything that I
want/need with respect to escaping (or not), so if that can be made to
work with minimal "tweaking", it would be ideal.

> Cheers,
> 
> 	f
> 

Thanks again for your time, dedication, and assistance. I'm very grateful!

-Ben



More information about the nginx mailing list