Skip to content

Commit 24e8c86

Browse files
committed
Add yii2
1 parent 71afa6a commit 24e8c86

File tree

1 file changed

+257
-0
lines changed

1 file changed

+257
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
---
2+
author: Pantelis Roditis
3+
date: 22/04/2022
4+
tags:
5+
- Yii2
6+
- NGINX
7+
---
8+
# Yii2 + NGINX custom selective dynamic error pages
9+
Ok this title makes no sense so i'll try to explain as best i can.
10+
11+
So we have a yii2 application served by nginx and fastcgi. Now as all clean url guides instruct out there, we've configured our nginx server in such a way to catch all requests and sent them to our application following a specific format.
12+
13+
So this means that when a user requests `/targets` the request internally gets translated to `/index.php?r=/targets`. So far so good.
14+
15+
## How it begun
16+
Now nginx also allows you to configure custom error pages and not those god forsaken white pages that kill your eyes. Lets not kid our selfs, these error pages, aesthetically, they look nothing like the rest of your site which make it look unprofessional and in some rare cases attract the wrong type of people to pay attention at our website.
17+
18+
So we went to configure our nginx to use our existing application logic for non existent files and other request errors. The way we did that was by something like the following
19+
```
20+
error_page 404 /index.php?r=$request_uri;
21+
```
22+
23+
One for each of the error codes that interest us, until we hit the error `50x` error status codes. Now usually these errors mean that there is something seriously wrong with the server trying to fulfill the request and as such we dont want to redirect it to back to our PHP application but rather to a static page that will hide our failures and embarrassment 😂
24+
25+
So we went and did something like this
26+
```
27+
error_page 500 501 502 503 ... /maintenance.html;
28+
```
29+
30+
Now these may seem self explanatory until you realize that these error pages are only for the ones that NGINX generates. What that means is that if you try to access the `/something/nonexistant/file.blah`, nginx will detect that this file does not exist and redirect your to the page defined by your `error_page` directive. Similarly yii when producing its own errors uses the same pattern and thus you achieve a nice error page throughout.
31+
32+
However, one thing that we need to keep in mind is that errors that are produced by your fastcgi applications are not processed. This is actually a feature, and allows applications to provide their own status code along with proper error pages. This is how we can show the 404 page and send the same status code as well to the browser, when a user requests a target page that does not exist.
33+
34+
What happens however when your application "crashes" for whatever unexpected reason?
35+
* Say you have a bug that causes your application to crash and cant serve the error pages like it normaly does
36+
* Say your system got rebooted and your php files got corrupted somehow
37+
* Say you wanted to make a change on your live system (you such if you did) and you have a typo
38+
* Say you updated your php and now its missing modules that you're using
39+
40+
In all those situations, you would expect that as long as your application produces a proper status code (eg `Status: 500 Application error`) for nginx to pick up and use the page we defined. But this is not what happens. You get the dreaded white page with the error in `<H1>` saying your application made a 💩
41+
42+
Luckily nginx has a way to do that, with the parameter `fastcgi_intercept_errors [on|off];`. This tells nginx that if our application produces any statis code larger than 300, then nginx should handle the necessary actions for this code.
43+
44+
## The problem is...
45+
So you go and put that into your config and suddenly all errors have turned into white pages except the ones that go to the static html (like `maintenance.html`).
46+
47+
Whats worse, your nice 404 pages are now back to their ugly white ones...
48+
49+
WTF?
50+
51+
Well this is the reason this is happening:
52+
1. User visits invalid bage (eg `/target/invalid`)
53+
2. The php application produces `404 target page not found`
54+
3. nginx receives a status code of 404 from your application
55+
4. `intercept_errors` kicks in which directs nginx to try and show the error page from your application (`/index.php?r=$request_uri`)
56+
5. but your app, still returns proper status code (404) which go back to nginx again
57+
6. nginx thinks that the `error_page` you defined doesnt actually exist (it returned 404) and shows you the default ugly and white one
58+
59+
So it seems that you cant have both ways, or can you?...
60+
61+
## There is a way...
62+
Define two identical blocks of fastcgi proxy pass one with intercept and the other without.
63+
The one with the intercept runs your normal application, when an error page is produced the error page is intercepted by nginx and is redirected to the other fastcgi block without intercept for your application to show and return proper error codes.
64+
65+
## Show me the 💶
66+
The following snippet provides a few extra tricks such as:
67+
* It only allows serving of`/index.php`
68+
* Any other php file returns 404
69+
* Removes duplicate `//` from the url
70+
* Rate limits certain urls and sets the status code to 429 for rate and connection limits
71+
* Doesnt allow direct access of static error pages
72+
* Sets static html for error codes of `50x` to `/maintenance.html`
73+
* All 4xx error codes are served by our application (from another fastcgi block)
74+
75+
76+
The following is the full server block we use for our applications
77+
```nginx
78+
server {
79+
listen {{item.ip}}:443 ssl;
80+
server_name {{item.domain}};
81+
root {{item.root}};
82+
ssl_prefer_server_ciphers on;
83+
ssl_session_timeout 5m;
84+
85+
ssl_certificate /etc/nginx/{{item.domain}}-server.crt;
86+
ssl_certificate_key /etc/nginx/{{item.domain}}-server.key;
87+
ssl_dhparam /etc/ssl/private/dhparam.pem;
88+
89+
ssl_ciphers 'AES256+EECDH:AES256+EDH:!aNULL';
90+
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
91+
ssl_session_cache builtin:1000 shared:SSL:10m;
92+
93+
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
94+
add_header X-Content-Type-Options nosniff always;
95+
add_header X-XSS-Protection "1; mode=block" always;
96+
add_header X-Frame-Options DENY always;
97+
98+
error_page 400 401 402 403 404 405 406 407 408 409 @errorphp;
99+
error_page 410 411 412 413 414 415 416 417 418 419 @errorphp;
100+
error_page 420 421 422 423 424 425 426 427 428 @errorphp;
101+
# serve flooders with static html
102+
error_page 429 @maintenance;
103+
104+
error_page 430 431 432 433 434 435 436 437 438 439 @errorphp;
105+
error_page 440 441 442 443 444 445 446 447 448 449 @errorphp;
106+
error_page 450 451 452 453 454 455 456 457 458 459 @errorphp;
107+
error_page 460 461 462 463 464 465 466 467 468 469 @errorphp;
108+
error_page 470 471 472 473 474 475 476 477 478 479 @errorphp;
109+
error_page 480 481 482 483 484 485 486 487 488 489 @errorphp;
110+
error_page 490 491 492 493 494 495 496 497 498 @errorphp;
111+
# 499 is only allowed to be used by nginx
112+
# timeout and bad gateway errors
113+
error_page 502 503 @maintenance;
114+
# php errors
115+
error_page 500 @maintenance;
116+
# not implemented
117+
error_page 501 @maintenance;
118+
# Gateway timeout
119+
error_page 504 @maintenance;
120+
# version not supported
121+
error_page 505 @maintenance;
122+
123+
124+
# this is needed for redirect to work
125+
merge_slashes off;
126+
# this ensures our urls are cleaned of multiple slashes
127+
rewrite (.*)//(.*) $1/$2 permanent;
128+
129+
# Our maintenance block
130+
location @maintenance {
131+
add_header Retry-After 600 always;
132+
# We need to include these again for the retry header to not remove them from above
133+
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
134+
add_header X-Content-Type-Options nosniff always;
135+
add_header X-XSS-Protection "1; mode=block" always;
136+
add_header X-Frame-Options DENY always;
137+
# Map everything to /maintenance.html
138+
rewrite ^(.*)$ /maintenance.html break;
139+
# This is internal redirect
140+
internal;
141+
}
142+
143+
# disable direct access to maintenance.html
144+
location = /maintenance.html {
145+
return 418;
146+
}
147+
148+
# for letsencrypt
149+
location /.well-known/acme-challenge/ {
150+
rewrite ^/.well-known/acme-challenge/(.*) /$1 break;
151+
root /acme;
152+
}
153+
154+
# Hide .htpasswd and .htaccess files as well
155+
# as any file starting with .ht
156+
location ~ /\.ht {
157+
return 404;
158+
}
159+
160+
# avoid processing of calls to non-existing static files by yii
161+
# and if they exist serve them directly with some caching
162+
location ~* \.(?:ico|css|woff2|exe|fla|gif|jpe?g|jpg|js|mov|pdf|png|rar|svg|swf|tgz|txt|xml|zip)$ {
163+
try_files $uri =404;
164+
access_log off;
165+
log_not_found off;
166+
expires 1M;
167+
add_header Cache-Control "public";
168+
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
169+
add_header X-Content-Type-Options nosniff always;
170+
add_header X-XSS-Protection "1; mode=block" always;
171+
add_header X-Frame-Options DENY always;
172+
}
173+
174+
set $yii_bootstrap "/index.php";
175+
176+
# Rate limit /api to 10 requests/sec
177+
# You need to add the following to your http { } nginx block
178+
# limit_req_zone $binary_remote_addr zone=Api:1m rate=10r/s;
179+
location /api/ {
180+
limit_req_status 429;
181+
limit_conn_status 429;
182+
limit_req zone=Api;
183+
index index.php;
184+
try_files $uri $uri/ /index.php$is_args$args;
185+
}
186+
187+
location / {
188+
index index.html index.php;
189+
try_files $uri $uri/ /index.php$is_args$args;
190+
}
191+
192+
# return 404 for direct access to php files
193+
location ~ \.php$ {
194+
return 404;
195+
}
196+
197+
location = /index.php {
198+
try_files $uri =404;
199+
fastcgi_split_path_info ^(.+\.php)(.*)$;
200+
201+
# let yii catch the calls to non-existing PHP files
202+
set $fsn /$yii_bootstrap;
203+
set $yiiargs r=$request_uri;
204+
if (-f $document_root$fastcgi_script_name){
205+
set $fsn $fastcgi_script_name;
206+
set $yiiargs $query_string;
207+
}
208+
209+
fastcgi_pass {{item.fpm}};
210+
include fastcgi_params;
211+
fastcgi_param SCRIPT_FILENAME $document_root$fsn;
212+
213+
# NGiNX allowed duplicate HOST headers to be set, the following is
214+
# a workaround for these cases.
215+
fastcgi_param HTTP_HOST "{{item.domain}}";
216+
217+
#PATH_INFO and PATH_TRANSLATED can be omitted, but RFC 3875 specifies them for CGI
218+
fastcgi_param PATH_INFO $fastcgi_path_info;
219+
fastcgi_param PATH_TRANSLATED $document_root$fsn;
220+
fastcgi_param QUERY_STRING $yiiargs;
221+
fastcgi_intercept_errors on;
222+
}
223+
224+
location @errorphp {
225+
internal;
226+
fastcgi_split_path_info ^(.+\.php)(.*)$;
227+
228+
# let yii catch the calls to non-existing PHP files
229+
set $fsn /$yii_bootstrap;
230+
set $yiiargs r=$request_uri;
231+
if (-f $document_root$fastcgi_script_name){
232+
set $fsn $fastcgi_script_name;
233+
set $yiiargs $query_string;
234+
}
235+
236+
fastcgi_pass {{item.fpm}};
237+
include fastcgi_params;
238+
fastcgi_param SCRIPT_FILENAME $document_root$fsn;
239+
240+
# NGiNX allowed duplicate HOST headers to be set, the following is
241+
# a workaround for these cases.
242+
fastcgi_param HTTP_HOST "{{item.domain}}";
243+
244+
#PATH_INFO and PATH_TRANSLATED can be omitted, but RFC 3875 specifies them for CGI
245+
fastcgi_param PATH_INFO $fastcgi_path_info;
246+
fastcgi_param PATH_TRANSLATED $document_root$fsn;
247+
fastcgi_param QUERY_STRING $yiiargs;
248+
fastcgi_intercept_errors off;
249+
}
250+
}
251+
```
252+
253+
This is still under a lot of development and fine tuning but it will save you the hair pulling 😀
254+
255+
## TODO
256+
* Set proper pages for 429, 500, 501, 502
257+
* https://en.wikipedia.org/wiki/List_of_HTTP_status_codes

0 commit comments

Comments
 (0)