Du pain, du vin, du PHP

Le gars dans la pub là

Ave visiteur !

Si toi aussi tu rêves d'utiliser un serveur Web moderne, avec des belles lignes de configuration toutes propres et fourni avec les piles, ça fait probablement un moment que tu t'intéresses à nginx (prononce… euh… prononce comme tu veux en fait). Le serveur Web venu du pays de Tonton Staline envoit sévèrement du pâté quand il s'agit de servir des contenus statiques ou de faire des reverse proxies avec des poils autour, mais il a un très gros défaut pour les débutants : il n'interprète pas directement les CGI ou le PHP.

Il faut donc se coltiner à la mano des processus PHP pour interpréter tout le caca qu'il y a besoin d'interpréter… et il faut vachement se méfier de ce qu'on lui fait bouffer.

PHP rapide interface de passerelle commune

Pour faire tourner du PHP sur nginx (prononce… mouais enfin, vaut mieux pas le prononcer en fait), il faut donc commencer par lancer ce qui concerne PHP grâce à spawn-fcgi avec un machin qui ressemble vaguement à ça :

/usr/bin/spawn-fcgi -s /var/run/fastcgi-php.run -U www-data -u www-data -g www-data -f /usr/lib/cgi-bin/php -P /var/run/fastcgi-php.pid

Ça lance donc une processus d'interprétation de code PHP (CGI) le tout avec l'utilisateur Web standard sous Debian (oui, je fais mes tests sous Debian et j'assume) vers une socket Unix toute propre /var/run/fastcgi-php.run. Pour lancer plusieurs processus de ce type, il faut ajouter quelques variables d'environnement avant la commande :

PHP_FCGI_MAX_REQUESTS=500
PHP_FCGI_CHILDREN=4

Donc, 5 processus (papa et 4 fistons) et 500 requêtes maximum par processus.

Simple, efficace, pas cher. Au niveau de nginx (pro… nan mais sérieusement, ils auraient pas pu trouver un meilleur nom ?), il faut mettre les bons paramètres chaque fois qu'on veut interpréter un fichier PHP. Voici un exemple qu'on trouve un peu partout sur le net :

location ~ \.php$ {
    fastcgi_pass unix:/var/run/fastcgi-php.run;
    include fastcgi_params;
}

À vue de pied pas trop de soucis : chaque fois qu'nginx (bon ok, j'arrête) croise une URL qui se termine en .php, il balance le tout dans la socket Unix (et quelques autres paramètres à la con…). C'est là qu'on va commencer à se fâcher.

bad interpreter

En effet, le problème avec cette configuration, c'est que nginx peut balancer à peu près n'importe quoi à PHP. Mais vraiment. Et sans réfléchir PHP va aller interpréter le premier truc cohérent dans le chemin qu'on lui envoit (va falloir qu'on discute un jour de ce qui est « cohérent »). Il est donc tout à fait possible de former une URL de ce type :

http://buttse.cx/image.png/test.php

Et d'aller donc interpréter une image dans PHP. Ouais, c'est moche. Premier truc donc, intercaller ce qu'il faut comme bout de code pour que ça ne puisse pas arriver. Et la magie try_files entre en action :

location ~ \.php$ {
    try_files $uri $uri/ =404;
    fastcgi_pass unix:/var/run/fastcgi-php.run;
    include fastcgi_params;
}

Là, impossible de baiser le système : si l'URL est foireuse, nginx renvoit 404 dans la face du monsieur en l'éclaboussant en prime. Maintenant, le souci, c'est que de temps en temps, il y a besoin d'avoir des URLs qui ne se terminent pas en .php mais qui doivent quand même être envoyées vers un fichier PHP.

Genre au pif, sur un blog quelconque http://buttse.cx/post/2012/02/OMG-Un-butt-plug-sans-fil doit être en réalité être interprété par index.php.

L'exemple que l'on trouve généralement est le suivant :

location / {
    try_files $uri $uri/ /index.php$uri?$args;
}

## tout ce qui n'est pas statique (dossier ou fichier) est converti en URL et envoyé dans index.php
location ~ ^(.+\.php)(/.*)?$ {
    fastcgi_split_path_info ^(.+\.php)(/.*)$;
    fastcgi_pass unix:/var/run/fastcgi-php.run;
    include /etc/nginx/fastcgi_params;
}
## tout ce qui se termine par .php ou avec des trucs derrière est interprété

Et boum !! Même arnaque que précédemment ! Si je tape l'innocente URL suivante :

http://buttse.cx/image.png/tamere.php/youpi/troll

PHP va de nouveau aller piétiner les baloches des autres. Et le pire, c'est que je ne peux pas me permettre de mettre un try_files avant, parce que justement le fichier PHP ne correspond pas au bout de l'URL en question. Je pourrais mettre les deux règles bout-à-bout : dans le bon ordre, cela permettrait de « limiter » le problème. Mais je pourrais toujours envoyer des saloperies à des scripts PHP légitimes, genre :

http://buttse.cx/admin.php/youpi/troll/face/lol/catz

Sauve-nous !

La seule solution que j'ai trouvée (et probablement pas la plus élégante) consiste à spécifier systématiquement les URLs qui ont besoin de PATH_INFO pour fonctionner. Ça donne un truc un peu crade :

location ~ ^/(index|menu|path|images).php(/.*)+ {
    fastcgi_split_path_info ^(.+\.php)(/.*)$;
    fastcgi_pass unix:/var/run/fastcgi-php.run;
    include fastcgi_params;
}
## les scripts index.php, menu.php, path.php et images.php suivi de paramètres sous forme d'URLs (obligatoire à cause du +)
## sont interprétés en PATH_INFO
location ~ \.php$ {
    try_files $uri $uri/ =404;
    fastcgi_pass unix:/var/run/fastcgi-php.run;
    include fastcgi_params;
}
## les autres fichiers PHP sont interprétés directement en vérifiant qu'ils existent

Cela impose de connaître tous les scripts qui ont besoin d'utiliser du PATH_INFO et leur chemin complet pour ne pas se faire empapaouter par le premier crypto-terroriste venu. C'est donc particulièrement lourd, même si nginx permet d'arriver à tout faire tenir en un seul location.

Voilà, maintenant que j'ai sauvé l'humanité, je vais aller faire une sieste.