OpsFire: Strip extensions from web URLs
Let's say that one day your devs make a Massively Soon-to-Be Popular PHP website, MSPP for short, and you find yourself the recipient of a request like this:
Hey the MSPP uses PHP but we don't like extensions. Can you strip the extensions from the web links?
"Sure thing!" you say to you, and after some reading you create something resembling the following /etc/nginx/conf.d/mspp.conf
file:
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}
server {
listen 443;
ssl on;
ssl_certificate /etc/pki/tls/certs/wildcard.example.com.crt;
ssl_certificate_key
/etc/pki/tls/private/wildcard.example.com.key;
server_name example.com;
root /var/www/php/mspp/;
index index.php;
location / {
try_files $uri @extensionless-php;
}
location @extensionless-php {
rewrite "^(.*)__aSyNcId_<_kvDkUaVB__quot; $1.php last;
}
location ~ \.php$ {
try_files $uri =404;
fastcgi_pass 127.0.0.1:9000;
include /etc/nginx/fastcgi.conf;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
include global/restrictions.conf;
include global/global.conf;
}
Quick review: in the above scenario you have cloned MSPP's code into the /var/www/php/mspp
directory and set the index file to be index.php
. You also have generated some SSL certs and are using those, right? (If not, different topic for a different day. But you really should do that.)
So.
You decide to open your browser and give the above configuration a whirl and go to:
https://example.com/about
LO! The about page appears. Then you try going to just the main page:
https://example.com/
And see a 404 error. Wait, what? Well, what happens if you add "index" to the URL?
https://example.com/index
This properly renders /index.php
, the index page. But wait, you say to you, didn't you set the index
to be index.php
? Is it not finding the index?
A glance in /var/log/nginx/error.log
shows this error:
2017/09/06 16:53:24 [error] 3513#0: *2 FastCGI sent in stderr: "Primary script unknown" while reading response header from upstream...
So when you try to hit the root directory, it's not properly finding the index page. You suspect that extensionless-php
is throwing something off, so you try commenting out that block and remove it from the try_files
. Now you can load the following properly:
https://example.com/
https://example.com/index.php
https://example.com/somepage.php
So everything works, including index index.php
, it's just not extensionless
. So let's reintroduce extensionless-php
and with a flicker of intuition let's also create a .php
file (nameless file) to test if extensionless
is just rewriting the .php
extension with an empty file name. Testing with /var/www/php/mspp/.php
:
<?php phpinfo(); ?>
Reload https://example.com/
and voilà!, the /.php
file loads.
Looks like that intuition was spot on: the request URL doesn't explicitly include the index
page, so when nginx hits the /
block and goes to extensionless-php
the URL is rewritten to /.php
which, until just now, did not exist. Then the request proceeds to the \.php$
block and FastCGI tries to execute the previously non-existent .php
file. The result was in the browser you saw the now-expected 404 for a missing file and you also saw the script unknown
error for a missing script in the nginx error log.
So how should you handle this situation? One way to handle it is to just make a another block that handles that specific case, e.g.:
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}
server {
listen 443;
ssl on;
ssl_certificate /etc/pki/tls/certs/wildcard.example.com.crt;
ssl_certificate_key
/etc/pki/tls/private/wildcard.example.com.key;
server_name example.com;
root /var/www/php/mspp/;
index index.php;
location / {
try_files $uri @extensionless-php;
}
location \/\.php {
try_files $uri =404;
fastcgi_pass 127.0.0.1:9000;
include /etc/nginx/fastcgi.conf;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$/index.php;
}
location @extensionless-php {
rewrite "^(.*)__aSyNcId_<_kvDkUaVB__quot; $1.php last;
}
location ~ \.php$ {
try_files $uri =404;
fastcgi_pass 127.0.0.1:9000;
include /etc/nginx/fastcgi.conf;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
include global/restrictions.conf;
include global/global.conf;
}
The \/\.php
block explicitly sets example.com/.php
to use index.php
. A potential problem: what if some day MSPP has subdirectories each with their own index pages? We can make this a little saner, and smarter, by eliminating that \/\.php
block and just adding a second rewrite
to the extensionless-php
block:
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}
server {
listen 443;
ssl on;
ssl_certificate /etc/pki/tls/certs/wildcard.example.com.crt;
ssl_certificate_key
/etc/pki/tls/private/wildcard.example.com.key;
server_name example.com;
root /var/www/php/mspp/;
index index.php;
location / {
try_files $uri @extensionless-php;
}
location @extensionless-php {
rewrite "^(.*)/__aSyNcId_<_kvDkUaVB__quot; $1/index.php last;
rewrite "^(.*)__aSyNcId_<_kvDkUaVB__quot; $1.php last;
}
location ~ \.php$ {
try_files $uri =404;
fastcgi_pass 127.0.0.1:9000;
include /etc/nginx/fastcgi.conf;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
include global/restrictions.conf;
include global/global.conf;
}
What's cool about this is the regex in that new rewrite
directive will match anything that ends in /
, even example.com/ohhaithere
.
Even more explicitly: if in the future the devs get a request to write some pages for a Testimonials section - marketing loves that right? - and you find yourself with a subdirectory tree like this:
mspp/
index.php
about.php
testimonials/
index.php
testimonial1.php
testimonial2.php
You won't need to adjust your nginx config at all for example.com/testimonials
and example.com/testimonials/testimonial1
to both load just fine and extension free since you're rewrite
ing all /
as /index.php
.
Documented on my frequently used assets page.