Nginx config for single page app with HTML5 App Cache - redirect

I'm trying to build a single page app that utilizes HTML5 App Cache, which will cache a whole new version of the app for every distinct URL, thus I must redirect everyone to / and have my app route them afterward (this is the solution used on devdocs.io).
Here's my nginx config. I want all requests to send a file if it exists, redirect to my API at /auth and /api, and redirect all other requests to index.html. Why is the following configuration causing my browser to say that there is a redirect loop? If the user hits location block #2 and his route doesn't match a static file, he's sent to location block #3, which will redirect him to "/" which should hit location block #1 and serve index.html, correct? What is causing the redirect loop here? Is there a better way to accomplish this?
root /files/whatever/public;
index index.html;
# If the location is exactly "/", send index.html.
location = / {
try_files $uri /index.html;
}
location / {
try_files $uri #redirectToIndex;
}
# Set the cookie of the initialPath and redirect to "/".
location #redirectToIndex {
add_header Set-Cookie "initialPath=$request_uri; path=/";
return 302 $scheme://$host/;
}
# Proxy requests to "/auth" and "/api" to the server.
location ~* (^\/auth)|(^\/api) {
proxy_pass http://application_upstream;
proxy_redirect off;
}

That loop message suggests that /files/whatever/public/index.html doesn't exist, so the try_files in location / doesn't find $uri when it's equal to /index.html, so the try_files always internally redirects those requests to the # location which does the external redirect.
Unless you have a more complicated setup than you've outlined, I don't think you need to do so much. You shouldn't need external redirects (or even internal redirects) or server-side cookie sending for a one-file js app. The regex match for app and api wasn't quite right, either.
root /files/whatever/public;
index index.html;
location / {
try_files $uri /index.html =404;
}
# Proxy requests to "/auth" and "/api" to the server.
location ~ ^/(auth|api) {
proxy_pass http://application_upstream;
proxy_redirect off;
}

Related

Serving files with PocketBase

What I want is to restrict access to files for unauthorized user.
PocketBase documentation says I can retrieve the file URL and access files through it. The example URL for a file would be like this:
http://127.0.0.1:8090/api/files/example/kfzjt5oy8r34hvn/test_52iWbGinWd.png
I can prevent unauthorized users to get this URL, but authorized users can share URL with other one.
Any ideas?
I found a good way to secure files with nginx, by adding an extra location for my PocketBase server block and using an extra backend with one endpoint.
So, my nginx looks like this:
server {
listen 80;
server_name example.com;
location /api/files {
proxy_intercept_errors on;
error_page 404 = #fallback;
proxy_pass http://127.0.0.1:5000;
}
location / {
proxy_pass http://127.0.0.1:8090;
}
location #fallback {
proxy_pass http://127.0.0.1:8090;
}
}
Where my expressjs backend working on port :5000 checks JWT and responds with 404 if it is valid. Nginx will redirect to :8090 (PocketBase) if 404 returned on :5000.

Files served with Aqueduct don't have a Content-Length header

I am writing a backend for my Flutter app using Aqueduct. I have Aqueduct set up so that Nginx proxies requests to it like this:
server {
root /home/web/my_server/web;
index index.html index.htm;
server_name example.com www.example.com;
location / {
try_files $uri $uri/ =404;
}
location /api {
proxy_set_header X-Real-IP $remote_addr;
proxy_pass http://127.0.0.1:8888;
proxy_http_version 1.1;
}
...
}
In Aqueduct I serve the files using a FileController:
router.route("/api/v1/app/*")
.link(() => LogController(context))
.link(() => FileController("public/app/")
..setContentTypeForExtension('apk', ContentType('application', 'vnd.android.package-archive')));
However, any files that it returns don't include the Content-Length header. That means that I can't show download progress.
I tried creating a custom FileController where I added the headers in manually:
final contentLengthValue = file.lengthSync();
return Response.ok(byteStream,
headers: {HttpHeaders.lastModifiedHeader: lastModifiedDateStringValue,
HttpHeaders.contentLengthHeader: contentLengthValue,
'x-decompressed-content-length': contentLengthValue,
HttpHeaders.cacheControlHeader: 'no-transform',
HttpHeaders.acceptRangesHeader: 'bytes'})
..cachePolicy = _policyForFile(file)
..encodeBody = false
..contentType = contentType;
The Content-Length header was still removed, but the x-decompressed-content-length header remained so this is a possible workaround. It just doesn't play nicely with some Flutter plugins that look for the Content-Length header and don't have a convenient way to check other headers.
Is this an Aqueduct problem or an Nginx problem? How do I solve it?
This solution works, but it skirts around the original problem. That it, it allows you to serve files that have the Content-Length in the header, but it doesn't explain why it was getting stripped in Aqueduct. Other answers are welcome.
Rather than have Aqueduct serve files, just have Nginx serve them directly.
If you can't change your API route, you can just give it an alias in the Nginx config location block. Add this before the /api location block.
location /api/v1/app/ {
alias /home/web/my_server/public/app/;
}
Now files in the app/ folder will get served by Nginx rather than Aqueduct. Nginx includes the Content-Length header in the files it returns.

Don't serve static files if backend is offline

I have the following nginx config that handles serving my static website and redirecting requests to my REST backend:
server {
listen 80 default_server;
server_name _;
# Host static content directly
location / {
root /var/www/html;
index index.html;
try_files $uri $uri/ =404;
}
# Forward api requests to REST server
location /api {
proxy_pass http://127.0.0.1:8080;
}
}
If my REST backend goes offline the proxy module returns an HTTP status of "502 Bad Gateway" and I can redirect requests to a status page by adding the following:
# Rewrite "502 Bad Gateway" to "503 Service unavailable"
error_page 502 =503 #status_offline;
# Show offline status page whenever 503 status is returned
error_page 503 #status_offline;
location #status_offline {
root /var/www/html;
rewrite ^(.*)$ /status_offline.html break;
}
However, this will only work for requests that access the REST backend directly. How can I redirect requests to my static website in the same way whenever the backend is offline?
Nginx does have some health check and status monitoring capabilities that seem like they could be related, but I couldn't find a proper way to use them.
While its intended use case is actually for authorization, I found nginx's auth_request module to work for me:
# Host static content directly
location / {
# Check if REST server is online before serving site
auth_request /api/status; # Continues when 2xx HTTP status is returned
# If not, redirect to offline status page
error_page 500 =503 #status_offline;
root /var/www/html;
index index.html;
try_files $uri $uri/ =404;
}
It will call /api/status as a subrequest before serving the static content and will only continue when the subrequest returns an HTTP status in the 200 range. It seems to return status 500 when the server is offline.
This method might have some performance implications since you're now always doing an extra request, but that seems to be an inherent requirement of checking whether your service is online.
I think this is the correct answer - auth request is ideal for any situation where you want to "ping" a backend before returning the requested content.
I have used a similar scheme in the past for an nginx server where I wanted to check if an auth header was correct before proxying to an S3 bucket.

Automatic trailing slash redirect leaks internal IP in the Location header

When doing curl -ILk http://192.168.0.4/app the Location header of the 301 redirect is http://my-srv.local/app/ which is a server-local address. I've put server_name_in_redirect on everywhere to get rid of it, but the result is the same.
Config:
server {
server_name my-nginx;
server_name_in_redirect on;
set $endpoint http://my-srv.local;
location / {
server_name_in_redirect on;
proxy_pass $endpoint;
}
}
Note: the nginx version that I'm using doesn't have absolute_redirect yet.
Reference: Information leak with automatic trailing slash redirect
How to make it point to the correct URL or prevent the leak at least?
So it's the my-srv.local who generates that response (that is pretty reasonable). With the nginx 1.11.8+ it will probably generate relative redirects. In the meantime, the possible fix is to overwrite that redirect:
server {
server_name my-nginx;
set $endpoint http://my-srv.local;
location / {
proxy_pass $endpoint;
proxy_redirect $endpoint http://$host;
}
}

Redirect nginx config server_name to custom 404 error page

I'm new to nginx configs and have spent a lot of time googling so far. I'm trying to create a very basic nginx config file to be used in a "redirect" server.
Users will be required to point naked domains (example.com) by A-record to my redirect server IP address, and the 'www' record by CNAME to another server.
The purpose of the redirect server is to then perform a 301 redirect any/wildcard naked domains back to to the 'www' version of the domain so it can be properly handled by my other server.
But I also want to catch any misconfigured 'www' domains that are pointing to my server IP by A-record, and simply direct them to a custom error page on the redirect server with further instructions on how to set up their account correctly for my service.
Here's what I have. It works, but since I am new to writing configs I was wondering if there is a better way to handle the redirect to the custom error page in the first server block. TIA!
#redirect to error page if begins with 'www.'
server {
listen 80;
server_name ~^www.; #only matches if starts with 'www.'. Is this good enough?
rewrite ^(.*)$ /404.html; #is this the correct way to direct to a custom error page?
error_page 404 /404.html;
location = /404.html {
root /usr/share/nginx/html;
}
}
#no match, so redirect to www.example.com
server {
listen 80 default_server;
rewrite ^(.*)$ $scheme://www.$host$1 permanent;
}
Prefix/suffix server name matching is faster and easier than regexp.
Also, there is no reason to use rewrite. You want to return 404, so do it and nginx will do all the rest. BTW, with rewrite you will return 200 OK with content of /404.html instead of 404 Not Found.
So here it is:
server {
listen 80;
server_name www.*;
root /usr/share/nginx/html;
error_page 404 /404.html;
location / {
return 404;
}
location = /404.html {
internal;
}
}