nginx: root chosen based on path - regex

I want the following.
http://some.site/person1/some/path should access /home/person1/some/path (and http://some.site/person1 accesses /home/person1/index.html) and http://some.site/person2/some/path should access /home/person2/some/path (and http://some.site/person2 accesses /home/person2/index.html). There will be many personXes. It's important to use a regular expression to tell nginx where to find everything.
I tried coming up with a set of location, root and rewrite directives that would work. The closest I came was this for my sites-available/website.
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
root /some/default/root;
# Add index.php to the list if you are using PHP
index index.html index.htm index.nginx-debian.html;
location / {
# First attempt to serve request as file, then
# as directory, then fall back to displaying a 404.
try_files $uri.html $uri $uri/ =404;
}
location /person1 {
root /home/person1;
rewrite ^/person1(.*)$ $1 break;
}
}
This does what I want with all paths except for ones of the form http://some.site/person1. In this case, nginx doesn't access /home/person1/index.html like I want. Instead, the regex returns an empty string which nginx doesn't like (I see complaints in the nginx/error.log).

when you have common start root dir in /home, you can try with:
location ~* /person\d+ {
root /home;
}

Related

Nginx internal location ignores django completely and allows free access instead

I want to have a private media folder on my django website, accessible only to logged in users, so I got to know that I should handle authentication part on the django side, and file serving on the nginx side. However following internal location config examples I find it impossible to make it work. Nginx ignores django completely (only for the internal location case). Even if I don't have the url allowed in my urls.py and I have it listed as internal location in nginx, it will still be freely accessible to everybody.
I am posting my nginx configuration in hope that someone can find a mistake in it.
My expectation is that everything in /internal/ folder will not be accessible to anonymous users and it will only be accessible by the django application through X-Accel-Redirect header. Right now if I go to /internal/test.png in an incognito window it will show me the picture.
I am not posting my django code for now, since it is ignored anyway by nginx, so it must be the nginx config problem.
server {
server_name XXX.XX.XX.XXX example.com www.example.com;
location = /favicon.ico {
access_log off;
log_not_found off;
alias /home/user/myproject/static/favicon4.ico;
}
location /static/ {
root /home/user/myproject;
}
location /media/ {
root /home/user/myproject;
}
location / {
include proxy_params;
proxy_pass http://unix:/run/gunicorn.sock;
}
location /internal/ {
internal;
root /home/user/myproject;
}
root /home/user/myproject;
location ~* \.(jpg|jpeg|png|webp|ico|gif)$ {
expires 30d;
}
location ~* \.(css|js|pdf)$ {
expires 1d;
}
client_max_body_size 10M;
# below in this server block is only my Certbot stuff
}
P.S. I swapped identifiable data to X characters and basic names.
I had 2 more problems in this config and I will show everything I did to make it work. The original problem why nginx was ignoring django was in how nginx chooses which location block to use, as suggested by Richard Smith.
From nginx.org we can read:
To find location matching a given request, nginx first checks locations defined using the prefix strings (prefix locations). Among them, the location with the longest matching prefix is selected and remembered. Then regular expressions are checked, in the order of their appearance in the configuration file. The search of regular expressions terminates on the first match, and the corresponding configuration is used. If no match with a regular expression is found then the configuration of the prefix location remembered earlier is used.
And also:
If the longest matching prefix location has the “^~” modifier then regular expressions are not checked.
So regular expressions, if available, will be chosen first. ^~ modifier before prefix makes it chosen instead of regular expressions.
I changed location /internal/ { line to location ^~ /internal/ { and then I got 404 errors every time and no matter how I tried to access the files, but at least I knew nginx was going to this location.
The 2nd mistake was thinking that I can get away with using the same url as the folder name, or in other words, that I can put in my urls.py
path('internal/<path>', views.internal_media, name='internal_media')
together with
location ^~ /internal/ {
internal;
root /home/user/myproject;
}
in my nginx config.
I can't. The url must be different, because otherwise the url doesn't lead to django urls.py - it still leads to /internal/ location through nginx (again, due to how nginx chooses locations).
I changed my urls.py line to point to private url instead:
path('private/<path>', views.internal_media, name='internal_media')
and in the views.py file I redirect to /internal/:
def internal_media(request, path):
if request.user.groups.filter(name='team-special').exists():
response = HttpResponse()
response['X-Accel-Redirect'] = '/internal/' + path
del response['Content-Type'] # without this your images will open as text
return response
else:
raise PermissionDenied()
Aaaand this still didn't work. 404 errors every time. The 3rd mistake was forgetting about the combo of those two:
location / {
include proxy_params;
proxy_pass http://unix:/run/gunicorn.sock;
}
location ~* \.(jpg|jpeg|png|webp|ico|gif)$ {
expires 30d;
}
Now if I went to the url /private/test.jpg nginx didn't let me go to django, because location / is lower in priority than regular expressions, so location ~* took precedence and I never got to django. I noticed it by accident after a lot of time being frustrated, when I put the url incorrectly in incognito mode. When I went to /private/test.jp now I got a 403 forbidden error instead of 404.
It started working immediately when I commented out this.
location ~* \.(jpg|jpeg|png|webp|ico|gif)$ {
expires 30d;
}
location ~* \.(css|js|pdf)$ {
expires 1d;
}
So now internal files worked nicely, but I didn't have caching...
To fix that, I modified my /static/ and /media/ locations, but maybe I won't go into that here, since it is a different topic. I'll just post my full nginx config that works :)
Well, what you might want to also know is that:
~* tells nginx that we are writing a regular expression that is case insensitive
~ would tell nginx that we were writing a regular expression that is case sensitive
server {
server_name XXX.XX.XX.XXX example.com www.example.com;
location = /favicon.ico {
access_log off;
log_not_found off;
alias /home/user/myproject/static/favicon4.ico;
expires 30d;
}
location /static/ {
root /home/user/myproject;
expires 30d;
}
location /media/ {
root /home/user/myproject;
expires 30d;
}
location ~* \/(static|media)\/\S*\.(css|js|pdf) {
root /home/user/myproject;
expires 1d;
}
location ^~ /internal/ {
root /home/user/myproject;
internal;
expires 1d;
}
location / {
include proxy_params;
proxy_pass http://unix:/run/gunicorn.sock;
}
client_max_body_size 10M;
# certbot stuff
}

Nginx try_files not working with domain that appends trailing slash

I have a dockerised Django app where nginx uses proxy_pass to send requests to the Django backend. I am looking to pre-publish certain pages that dont change often so Django doesnt have to deal with them.
I am trying to use try_files to check if that page exists locally and pass to Django if not.
Our URL structure requires that all URLs end with a forward slash and we dont use file type suffixes e.g. a page might be www.example.com/hello/. This means the $uri param in nginx in this instance is /hello/ and when try_files looks at that it is expecting a directory due to the trailing slash. If I have a directory with a list of files how do I get try_files to look at them without re-writing the URL to remove the slash as Django requires it?
My nginx conf is below.
server {
listen 443 ssl http2 default_server;
listen [::]:443 ssl http2 default_server;
server_name example.com;
root /home/testuser/example;
location / {
try_files $uri uri/ #site_root;
}
location #site_root {
proxy_pass http://127.0.0.1:12345;
}
}
If I have a file "hello" at /home/testuser/example/hello and call https://www.example.com/hello/ how can I get it to load the file correctly?
P.S. the permissions on the static content folder and its contents are all 777 (for now to rule out permissions issues)
Cheers in advance!
You can point the URI /hello/ to a local file called hello or hello.html using try_files, but you must first extract the filename using a regular expression location. See this document for details.
The advantage of using .html is that you will not need to provide the Content-Type of the response.
For example, using hello.html:
root /path/to/root;
location / {
try_files $uri uri/ #site_root;
}
location ~ ^(?<filename>/.+)/$ {
try_files $filename.html #site_root;
}
location #site_root {
proxy_pass ...;
}
If you prefer to store the local files without an extension, and they are all text/html, you will need to provide the Content-Type. See this document for details.
For example, using hello:
root /path/to/root;
location / {
try_files $uri uri/ #site_root;
}
location ~ ^(?<filename>/.+)/$ {
default_type text/html;
try_files $filename #site_root;
}
location #site_root {
proxy_pass ...;
}
In my case using NextJS, leaving the final slash causes errors.
So here is the solution I found to make it work nicely:
root /path/to/static/files/directory; # no trailing slash
rewrite ^/(.*)/$ /$1 permanent; # automatically remove the trailing slash
location / {
try_files $uri $uri.html $uri/index.html /404.html;
# try the provided uri
# then try adding .html
# then try uri/index.html (ex: homepage)
# finally serve the 404 page
}

How to forward all paths that start with a specific location in nginx?

I want to forward all paths that start with /api/ (/api/* ??) to port 1000 but the actual configuration either forwards only the paths that contain "/api/" and nothing else after (/api/login is not forwarded)
location /api/ {
proxy_pass http://localhost:1000/;
}
or it doesn't work at all
location ~ ^/api/(.*)$ {
proxy_pass http://localhost:1000/;
}
. The server is cinfigured as fallows:
server {
listen 80;
keepalive_timeout 70;
server_name server_name;
location / {
root /var/www/html;
index index.html;
}
location /api/ {
proxy_pass http://localhost:1000/;
}
}
I would appreciate any help, Thank you!
Note that with:
location /api/ {
proxy_pass http://localhost:1000/;
}
If there is request /api/foo, then your API server will see /foo.
If, on the other hand (note there is no trailing slash in proxy_pass) you use:
location /api/ {
proxy_pass http://localhost:1000;
}
Then for the same request, your API server will receive request "as is": /api/foo.
So make sure you use the right approach (slash / no slash) which depends on how your API server handles URLs (if it is configured to handle /api/foo URLs then you should not use trailing slash in the proxy_pass.

Negative lookbehind in nginx location block

I need to create a virtual sub directory in nginx which will serve files from a different location than the root. This virtual directory also runs PHP, but I've omitted the fastcgi config for brevity.
I have two location blocks, the one for the /virtual directory uses alias, while the location block for / has it's own root directive. This root directive gets inherited by the virtual folder, messing up the paths. Is there any way I can exclude urls in /virtual from inheriting the root directive of the / location block?
Here is an excerpt of my server config:
server {
root /path/to;
location ^~ /virtual/ {
alias /path/to/lions/public;
try_files $uri $uri/ /lions/public/index.php?$query_string;
}
location / {
root /path/to/tigers/public;
try_files $uri $uri/ /index.php?$query_string;
}
}
If I browse to hostname/virtual/whatever, nginx looks for the file at
/path/to/tigers/public/lions/public/whatever
instead of
/path/to/lions/public/whatever
I have tried the following pattern to match all urls at / except for those starting with /virtual, but that didn't match anything.
location ^~ ((?!\/virtual)\/.*)$
I have also tried putting both an alias and a root directive in the location block for /virtual, but nginx wouldn't start.
Is there any other way to have a different root for my alias directory?
The value of the location and the value of the alias should either both end in / or neither end in / - due to the way nginx does the text substitution when calculating the $request_filename.
All elements in the try_files statement are URIs, so your final element should be /virtual/index.php?$query_string and not /lions/public/index.php?$query_string. The latter would begin a new search, which would be processed by the location / block. Hence the strange file path.
Finally, there is an unpredictability when using alias and try_files together. See this bug report. The same functionality can be achieved using a if block. See this caution on the use of if.
For example:
location ^~ /virtual/ {
alias /path/to/lions/public/;
if (!-e $request_filename) { rewrite ^ /virtual/index.php?$query_string; }
}

Nginx location block matching rule

I'm new to nginx server.
I'm gonna deploy the php framework such as codeigniter to the nginx server.
My config file is following.
server {
index index.html index.php index.htm;
# set expiration of assets to MAX for caching
location ~* \.(ico|css|js|gif|jpe?g|png)(\?[0-9]+)?$ {
expires max;
log_not_found off;
}
location / {
# Check if a file exists, or route it to index.php.
try_files $uri $uri/ /index.php;
}
location ~* \.php$ {
fastcgi_pass unix:/var/run/php/php7-fpm.sock;
fastcgi_index index.php;
fastcgi_split_path_info ^(.+\.php)(.*)$;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
My question is following.
1)The request uri is like this "www.domain.com/controllername/functionname/param1/param2/"
How does nginx work with this url?
2)The third location block matches the regular expression ".php$".
Is this true only if the uri has ended with ".php"?
(I think so , but that block's fastcgi_split_path_info has different regular expression.)
Question 1) Yes this should work, because the line
try_files $uri $uri/ /index.php;
is handled one by one for a request. First nginx tries to find the file described by the URI, if there's no match, it checks if it is a directory. If not it calls your index.php file. The original URI is handed over with a lot of other HTTP_REQUEST variables and the code from codeigniter takes care of parsing the url, if you config (codeigniter is correct).
The call convention for codeigniter is "www.domain.com/controllername/public_function/param1/param2/"
So normally you don't give the viewname, but the controller and the function name in your URI.
Question 2) The "location" directive only uses the URI path without any GET parameters. The split_path works differen and so it need a different regexp.