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_<_GyHwTHJe__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_<_GyHwTHJe__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_<_GyHwTHJe__quot; $1/index.php last;
        rewrite "^(.*)__aSyNcId_<_GyHwTHJe__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 rewriteing all / as /index.php.

Documented on my frequently used assets page.