Deblan blog

Développement

Outil de création d’un espace web Apache et PHP

Dans mon activité personnelle et professionnelle, je suis amené à créer des espaces d’hébergement de sites web principalement écrits en PHP.

Il y a quelques années, j’ai écris un script en shell qui posait des questions et générait des fichiers de configuration pour Apache et PHP puis relançait ces services. Il a ensuite évolué et générait également les utilisateurs unix et affinait les permissions. Le principal problème du script est que d’un serveur à l’autre, il fallait mettre des coups de hache dans le code pour l’adapter.

Cette semaine, j’ai entamé une refonte complète du code. Au fur et à mesure du développement, j’ai rendu pas mal de choses configurables et je pense qu’il est fonctionnel sur des environnements relativement différents des miens.

Je vous présente donc vhost-manager, c'est un projet libre et est toujours orienté vers la génération de vhost Apache et de pools PHP FPM. Il faut make, gcc, wget pour l'installer et sh, whiptail et php sont nécessaires à son utilisation.

vhost-manager

Le code source est disponible ici. Le projet se configure via un fichier de variables et j'ai conservé le principe des questions/réponses pour générer les fichiers.


API pour récupérer le contenu Open Graph d'une page web

Pour la fonctionnalité de partage de liens sur ce blog, j'ai développé un script qui récupére le contenu d'une page et analyse ses balises <meta> pour identifier ses données Open Graph.

Dans un autre contexte, j'ai rencontré un bug dans Wallabag qui l'empêche de récupérer le contenu de plusieurs pages web que je désirait lire plus tard. Avec la volonté de créer un rapport de bug, la documentation m'a amené sur la piste du projet Graby utilisé par Wallabag.

Pour comprendre et peut-être proposer un correctif, j'ai joué avec Graby et ça m'a amené à réaliser une API pour remplacer le script utilisé par le blog, en combinent Graby et fusonic/opengraph. L'objectif de l'API est donc de retourner des données générées par Graby et OpenGraph au format JSON.

Voici à quoi ressemble un retour d'appel à l'API avec les données Open Graph :

Le projet est dépendant de PHP 7.3 et c'est libre. Le code source est dispo ici.


Mes scripts i3blocks pour générer les éléments de ma barre i3-wm

i3-wm est le gestionnaire de fenêtres que j'utilise depuis quelques d'années maintenant. Au fur et à mesure du temps, j'ai fais évoluer mon interface et j'ai écris des scripts pour générer les informations affichées dans la barre du dessus.

i3-wm

Parce que je suis plus à l'aise avec ce langage, l'ensemble des scripts est écrit en PHP 7. Ces scripts sont exécutés au travers de i3blocks, l'utilitaire qui gère les exécutions et les rendus des blocs.

Le fichier de configuration a cette forme et voici les détails techniques des blocs. Ils incluent tous base/block.php et le code source est libre. Le code n'est pas toujours élégant mais ça fonctionne pas trop mal 😀

Dépendance globale des scripts : interpréteur PHP 7.

Bande passante

Affiche la bande passante UP et DOWN d'une interface réseau.

  • Code source
  • Dépendances : ip, grep, awk, cut, ifstat et tail
  • 1er paramètre : l'interface réseau (défault : eth0)
  • 2ème paramètre : inet pour l'IPV4 (par défaut) ou inet6 pour l'IPV6

IP locale

Affiche l'adresse IP d'une interface réseau. Au clic, l'IP est copiée dans le presse-papier.

  • Code source
  • Dépendances : ip, grep, awk, cut, xclip
  • 1er paramètre : l'interface réseau (défault : eth0)
  • 2ème paramètre : inet pour l'IPV4 (par défaut) ou inet6 pour l'IPV6

IP publique

Affiche votre adresse IP publique via une requête DNS à opendns.com. Au clic, l'IP est copiée dans le presse-papier.

  • Code source
  • Dépendances : dig et xclip
  • 1er paramètre : 0 pour l'IPV4 (par défaut) ou 1 pour l'IPV6

Espace libre

Affiche le pourcentage d'espace libre d'un point de montage. Au clic, le gestionnaire de fichiers affiche le répertoire du point de montage.

  • Code source
  • Dépendances : df, tail et xdg-open
  • 1er paramètre : le nom du point de montage (défault : root)
  • 2ème paramètre : le chemin vers le point de montage (défault : /)
  • 3ème paramètre : pourcentage du niveau d'alerte "moyen" (défault : 70)
  • 4ème paramètre : pourcentage du niveau d'alerte "critique" (défault : 90)

Spotify

Affiche l'artiste et le titre de la musique jouée. J'affiche le workspace 6. MEDIA lors d'un clic.

Affiche un bouton pour lancer ou mettre en pause le titre en court.

Volume

Affiche le volume du son.

Météo

Affiche la météo du lieu détecté par fr.wttr.in. Au clic, le site est affiché dans le navigateur web.

Application

Affiche une lettre colorée et lance un script quand on clique dessus. Dans ma configuration, ça affiche des sites web dans le navigateur web.

  • Code source
  • 1er paramètre : la lettre à afficher
  • 2ème paramètre : le script à exécuter au clic
  • 3ème paramètre : la couleur de fond (défaut : #333)
  • 4ème paramètre : la couleur du texte (défaut : #fff)

Heure et date

Affiche l'heure et la date.

Flux RSS

Affiche le nombre d'articles non lus. Au clic, le navigateur affiche le client web du votre lecture RSS (tt-rss chez moi).

  • Code source
  • Dépendance : xdg-open
  • 1er paramètre : URL du flux RSS
  • 2ème paramètre : adresse du lecture RSS (optionnel)

Batterie

Affiche l'état de charge de la batterie.


Équivalent du MATCH AGAINST de MySQL sur PostgreSQL

Le blog est propulsé sur un système de gestion de contenu écrit sur Symfony. Les données sont gérées dans une base MariaDB et ça tourne très très bien :)

Pour apprendre à utiliser PostgreSQL, je me suis donné comme défi de rendre compatible ce blog avec PostgreSQL. Fort heureusement, j'ai un ORM et 90% du boulot est géré par 3 lignes de configuration.

Le moteur de recherche est un peu plus compliqué à migrer puisque j'ai généré des requêtes en dehors de l'ORM. Son fonctionnement est relativement standard car quand un utilisateur saisi des mots clés, une première requête SQL va donner un score aux articles du blog publiés et je vais afficher ceux qui dépassent une valeur donnée.

Pour ce faire, j'utilise MATCH AGAINST de MySQL/MariaDB et la requête donne ça :

SELECT
    post.id,
    post.title,
    post.tags,
    MATCH(post.title) AGAINST(:search) AS MATCH_TITLE,
    MATCH(post.content) AGAINST(:search) AS MATCH_CONTENT,
    MATCH(post.tags) AGAINST(:search) AS MATCH_TAGS
FROM post
WHERE
    post.active = 1 AND
    post.published_at < :date
ORDER BY
    MATCH_TITLE DESC,
    MATCH_TAGS DESC,
    MATCH_CONTENT DESC

Pour obtenir des résultats équivalents avec PostgreSQL, la requête doit changer car MATCH AGAINST n'existe pas et comme PostgreSQL offre des outils beaucoup plus complets, c'est moins évident. Je trouve d'ailleurs que la documentation est assez peu claire à ce sujet. J'ai mis du temps à pondre une requête qui fonctionnait. La voici :

SELECT
    post.id,
    ts_rank(to_tsvector(post.title), query) as match_title,
    ts_rank(to_tsvector(post.tags), query) as match_tags,
    ts_rank(to_tsvector(post.content), query) as match_content
FROM
    post,
    plainto_tsquery(:search) query
WHERE
    post.active = true AND
    post.published_at < :date
ORDER BY
    match_title DESC,
    match_tags DESC,
    match_content DESC

Dans les 2 cas, :search correspond aux mots clés et :date représente la date où la recherche est faite.

Les scores ne sont pas du même ordre de grandeur mais je retrouve des résultats équivalents sur les 2 moteurs de base de données.


[tips] PHP FPM : récupérer la vraie adresse IP du visiteur

Mon serveur web fonctionne par couches. La première couche est gérée par Nginx et traite les requetes HTTP des internautes. Nginx gère les problématiques de cache sur les assets, c'est à dire les images, les fichiers javascripts et enfin les fichiers CSS. Ensuite, il transmet les requêtes au serveur web Apache qui va délivrer le site web concerné et faire appel au process manager de PHP (FPM) pour exécuter PHP.

En l'état, il n'est pas possible de connaître l'adresse IP de l'internaute via la variable globale $_SERVER en utilisant l'index REMOTE_ADDR. En effet, si j'affiche le contenu de $_SERVER['REMOTE_ADDR'], j'aurai comme résultat 127.0.0.1 qui est l'IP locale de Nginx.

Cependant, j'ai configuré Nginx de tel sorte qu'il ajoute les entêtes HTTP X-Forwarded-For et X-Real-IP. Apache les transmet ensuite via les index HTTP_X_FORWARDED_FOR et HTTP_X_REAL_IP.

proxy_set_header X-Real-IP  $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;

Ces index contiennent l'IP de l'internaute mais ne sont pas utilisés par les applications hébergées. Typiquement, Nextcloud détectait l'adresse IP 127.0.0.1. Lors d'un brute force, tous les comptes utilisateurs étaient impactés par la sécurité enclanchée par Nextcloud (soit 30s d'attente avant la validation des identifiants de connexion).

J'ai fouiné sur la toile et plusieurs solutions sont évoquées. J'ai un peu tout essayé et au delà des modules Apache Rpaf et RemoteIP, pas grand chose de probant. Rpaf est déjà installé et permet à Apache de logger la bonne IP dans les logs d'accès. RemoteIP n'a pas fonctionné dans mon environnement.

Pour résoudre le problème, j'ai appliqué ce qui a été rédigé dans ce post : inclure un script PHP de façon automatique pour l'ensemble des sites web du serveur.

j'ai créé un fichier PHP comme suit :

<?php

$trustedProxies = [
    '127.0.0.1',
];

$remote = $_SERVER['REMOTE_ADDR'];

$headers = [
    'HTTP_X_FORWARDED_FOR' => 'REMOTE_ADDR',
    'HTTP_X_REAL_IP' => 'REMOTE_HOST',
];

if (in_array($remote, $trustedProxies)) {
    foreach ($headers as $header => $value) {
        $_SERVER[$value] = $_SERVER[$header];
    }
}

Puis j'ai alimenté les php.ini de cette façon :

auto_prepend_file = /etc/php/fpm/real-ip.php

Après avoir relancé les services, j'ai pu constater que l'adresse IP de l'internaute est bien présente dans $_SERVER['REMOTE_ADDR'].