created by Michael Bladowski himself

A story about Caddy, Nginx, and some Laravel routing

Micha(el) Bladowski

--

This is just a sad story of me getting crazy with something that sounds so stupid and simple in the first place, that you think, it should be done in 5 minutes — but after several hours of trying, I gave up. Yes, I had to admit, that the Internet was not able to offer me a simple solution. Even asking ChatGTP and Bing(Chat) didn’t help. Or, it´s just me, who knows? I will explain. And yes, if you know the Holy Grail of how to make this work with Caddy, PLEASE let me, but before you write anything: please test it yourself, I am done with “this should work” recommendations ;)

Spoiler: I got some cool feedback from someone who showed me, how it´s done, more at the end.

The Background

I have written a Web-App that collects SMS from different Providers so employees of a company are able to get any 2-Factor-Authentification-SMS without always asking, which person currently has the mobile with a specific phone number. So many platforms offer a 2FA SMS, which is in general a good thing, don´t get me wrong — but a company with tons of employees has to struggle with that. In most cases, they are using a company mobile, maybe two or more… different phone numbers, and whenever someone has to log in somewhere, someone other in the company gets a call with the famous question: “hey sorry, can you please give me the code you just got on the number….?”

Now just imagine YOU are the person that gets 15 calls each day where someone is asking for a stupid code — annoying — right? okay… so now you understand why there has to be a solution for this.

First Solution

Rent an SMS number from a Service Provider that is able to push the SMS to a specific URL or Email. Nothing special.

A cheaper/better/more secure Solution

Yes, pardon, we get to the point about Caddy, nginx, routing, and all that, I just thought it might be interesting to see, where all that is coming from ;)
There is a very cool project called “httpsms” from “Acho Arnold”:

https://httpsms.com

The App is simple. It pushes every incoming SMS from your own mobile to the Server and the Server is able to forward (push) it to your specific URL.

Arnold has been so kind to offer us the opportunity to change the Server Hostname to any other domain, so we don´t need his Server and so we can push the SMS directly to — yes, finally…. to Laravel ;)

So instead of:

Android APP -> https://api.httpsms.com -> https://api.my-own-server.com/api/httpsms/….

we can do:

Android APP -> https://api.my-own-server.com/api/httpsms/…

That´s amazing, so we have full control and companies feel more
safety knowing, there is no longer a “man-in-the-middle”.

The Problem

The Android App only allows me to set the Host, but NOT the full URI.
So this is working, i.e.:

https://my-server.com

but this isn’t:

https://my-server.com/api/httpsms/

The App is sending everything to <HOST>/v1/…
For example, these are URLs the APP is calling:

GET  /v1/users/me
POST /v1/heartbeats
PUT /v1/phones
POST /v1/messages/receive

I am using a simple Caddy config:

:80 {
root * /app/public
encode zstd gzip
file_server
php_fastcgi phpfpm:9000
}

And now the fun begins.

https://tenor.com/de/search/rubbing-hands-gifs

What do we want? Simple. In case the URL contains /v1/ rewrite it to:
/api/httpsms/v1/.... .

Why? Because a default Laravel Project is using a separate routing file
for API Calls called /routes/api.php .
But only routes starting with /api/ are being watched here.

I now could write 30 Minutes just explaining why NOTHING was working for me, I rather post my Caddyfile, so you can see for yourself the result of my desperation:

:80 {
# uri replace /v1/ /api/httpsms/v1/
# rewrite /v1/* /api/httpsms/{http.request.uri.path}
root * /app/public
encode zstd gzip
file_server
# header_upstream X-Original-URI {uri}
# rewrite /v1/users/me /api/httpsms/v1/users/me?{query}&p={path}
# rewrite /v1/(.*) /api/httpsms/v1/{1}
uri replace /v1/ /api/httpsms/v1/
php_fastcgi phpfpm:9000 {
env REQUEST_URI {uri}
}
#{
# env PATH_INFO {http.request.uri.path}
# env REQUEST_URI {http.request.uri.path}
# index index.php
# }
# split the URI by .php
# split .php
# use index.php as the default script
# index index.php
# header_up X-Original-URI {uri}
# header_up Host {http.reverse_proxy.upstream.hostport}
# header_up X-Real-IP {http.reverse_proxy.upstream.remote}
# header_up X-Forwarded-For {http.reverse_proxy.upstream.remote}
# header_up X-Forwarded-Proto {http.reverse_proxy.upstream.scheme}
# header_up X-Forwarded-Host {host}
# header_up X-Forwarded-Port {http.reverse_proxy.upstream.port}
# }
log {
# output logs to standard error (console)
output stderr
}
}

Long story short: I searched the web, I asked ChatGPT, Bing, and everything, but nothing was working.

The Laravel route inside /routes/api.php never has been hit, because php-fpm never saw /api/sms/v1/... it always saw the original URL /v1/…
or “index.php” or something else, nothing was working.

I mean, what the heck? Why is php-fpm unable to catch fastcgi variables from Caddy? I have no idea.

After some hours of pure desperation, I gave up. I really like Caddy, it´s such a simple and easy web server and you only need 6 lines of config to make this run for a default PHP/Laravel Projekt.

But if you are not able to rewrite the URL, at this moment, I got mad!

https://tenor.com/de/view/crazy-insane-stewie-family-guy-finals-gif-14502837

This is one of these stupid developers' days where you want to try something that should take 2 Minutes and very shortly you realize, that´s something you are doing for the rest of the day ;-((( AHHHHHHHHH !!!!

I hate giving up, but after a few hours, I was done, no chance.

NGINX to the Rescue !

My good old friend NGINX, please show me how easy it is.
It turned out, it wasn´t that easy as well, because just putting some stuff inside the config didn´t work as well, because the ORDER is important.

Long story short, here’s an example of how all that “rewriting” is done with NGINX:

server {
listen 80 default_server;
server_name _;
root /app/public;

index index.php;

location ~ \.php$ {

try_files $uri /index.php =404;

set $path $request_uri;
if ($request_uri ~ ^/v1/(.*)$) {
set $path /api/httpsms/v1/$1;
}

fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass phpfpm:9000;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_param REQUEST_URI $path;

}

location / {
try_files $uri $uri/ /index.php?$query_string;
gzip_static on;
}

}

So as you might have seen yourself, the magic happens here:

....
set $path $request_uri;
if ($request_uri ~ ^/v1/(.*)$) {
set $path /api/httpsms/v1/$1;
}
....
....
fastcgi_param REQUEST_URI $path;

And finally…..

The routing was working as expected.
Every incoming Request like /v1/... got rewritten to /api/httpsms/v1/…
and so the Laravel API routing finally worked.

Again, if you know how this is working with Caddy, feel free to let me know. Please provide us with a docker-compose.yml and a Caddyfile with your solution, I would be more than happy to see, what I obviously couldn´t see that day ;-)

Update 01.06.2023: So I got some cool feedback from Francis Lavoie who showed me, how it´s done. THANKS, MAN!!! You are my hero! ;-)))

As always, I hope you learned something ;)
Feel free to Clap and Follow — and if you are interested in topics like
PHP, DevOps, Laravel, AI, Cool Tools… follow me on Twitter.

Cheers,
Micha

--

--