Suite à un article publié sur Debian-Administration je me suis lancé dans l'installation d'un serveur de capture de page web.
Suite à un article publié sur Debian-Administration je me suis remis à un ancien projet de serveur de capture de page web. Ce type de service existe déjà sur Internet mais je voulais un outils installé sur un serveur privé à l'intérieur d'un réseau d'entreprise pour avoir la possibilité de capturer le rendu de page web sur des serveurs load-balancés. Il faut donc que le serveur qui effectue les captures soit à l'intérieur du réseau privé de notre datacenter.
Après un rapide test du système proposé par l'article de Debian-Administration, j'ai voulu étendre le système pour avoir plusieurs captures en parallèle depuis le même serveur de capture. J'ai donc créé des environnements chrootés mais une limite de fonctionnement des applications (FireFox entre autre) ne permet pas de lancer plusieurs rendus en même temp. Il est donc nécessaire de passer à un système de virtualisation pour correctement isoler les processus.
Parmis les possibilités j'ai choisi la solution OpenVZ, très légère en terme de charge système ce qui me permet de recycler un vieux serveur.
Le système à mettre en place consiste à créer des containeurs OpenVZ avec un serveur Apache et quelques scripts php. Le containeur va faire tourner un serveur X en mémoire et sous un VNC dans lequel sera lancé un navigateur (firefox). Le rendu sera demandé par un client web (navigateur du poste de rendu) qui contrôlera l'exécution de firefox dans le containeur. L'image du site capturée sera renvoyée par Apache. Enfin un autre serveur Apache servira de système de distribution des requêtes vers les différents containeur. Ainsi chaque containeur fonctionne de manière autonome et peut être facilement ajouter ou retirer du pool de distribution, et il est aussi possible de distribuer les containeurs sur d'autres serveurs distants.
ATTENTION : aucune sécurité ne sera mise en place ! nous partons du principe que ce système de capture est à usage privé et donc nous ne parlerons pas de la sécurité (réseau et containeur).
Préparation du système
Installation d'OpenVZ
Note: il est tout à fait possible d'utiliser des machines virtuelles 'classiques' (KVM, XEN ...); dans ce cas vous devez simplement installer vos instances de manière classique et sauter ce paragraphe_ et le suivant.
Debian Squeeze fourni un noyau précompilé avec le support d'OpenVZ et quelques outils qu'il 'suffit' d'installer (je suis en 64bits) :
apt-get install linux-image-openvz-amd64 vzctl vzdump vzquota ln -s /var/lib/vz /vz vi /etc/sysctl.conf
et vérifiez que les paramètres suivants y sont définis :
net.ipv4.conf.all.rp_filter=1 net.ipv4.icmp_echo_ignore_broadcasts=1 net.ipv4.conf.default.forwarding=1 net.ipv4.conf.default.proxy_arp = 0 net.ipv4.ip_forward=1 kernel.sysrq = 1 net.ipv4.conf.default.send_redirects = 1 net.ipv4.conf.all.send_redirects = 0 net.ipv4.conf.eth0.proxy_arp=1
et il faut bien sûr rebooter le serveur pour appliquer ces changements en prenant soin de sélectionner le noyau OpenVZ.
Création des containeurs
Pour la création et l'administration des containeurs il peut être intéressant d'installer une interface graphique du type OVZ Web Panel. Pour l'instant nous allons utiliser les outils en ligne de commande.
Nous allons créer un seul containeur pour l'instant. Nous le clonerons par la suite.
Il faut créer le containeur à partir d'un template, ici on choisi une Debian Squeeze 32bits :
cd /var/lib/vz/template/cache wget http://download.openvz.org/template/precreated/debian-6.0-x86.tar.gz vzctl create 101 --ostemplate debian-6.0-x86 --config basic vzctl set 101 --hostname screenshoter-101.bogus.fr --save vzctl set 101 --ipadd 10.0.0.101 --save vzctl set 101 --kmemsize 8120000:8130000 --save vzctl set 101 --privvmpages 256000:258000 --save vzctl set 101 --diskspace 2000000:2100000 --save vzctl set 101 --numothersock 120 --save vzctl set 101 --nameserver 10.0.0.1 --save vzctl set 101 --onboot yes --save vzctl start 101
Pour vérifier la liste des containeurs et faire un petit tour :
vzlist -a vzctl enter 101 (exit pour quitter)
Système de capture des pages web
Configuration des outils
Attention ! nous entrons maintenant dans le containeur (ou la machine virtuelle) et la suite des commandes est à exécuter à l’intérieur de celui-ci grâce à la commande qui suit :
vzctl enter 101
Nous avons besoin d'un serveur VNC, d'Apache, de PHP, d'imagemagick et de Firefox.
En reprenant l'article de Debian Administration et en utilisant les dépôts de la Debian Mozilla Team pour installer une version récente de Firefox :
echo "deb http://mozilla.debian.net/ squeeze-backports iceweasel-release" > /etc/apt/sources.list.d/mozilla.list wget -O- -q http://mozilla.debian.net/archive.asc | gpg --import gpg --export -a 06C4AE2A | apt-key add - apt-get update apt-get install vnc4server xfonts-base xfce4 imagemagick apt-get install -t squeeze-backports iceweasel
Maintenant il faut démarrer le serveur X avec la résolution qui nous convient. Ici nous avons chois une largeur de 1280 et une hauteur importante (6000 pixels) pour avoir (si possible) le rendu de toute la page. Attention, nous démarrons le serveur X sous l'utilisateur du serveur Apache www-data car c'est sous ce compte que toutes les manipulations vont se faire :
Note: les 'grande hauteur' (8000 pixels) de l'affichage que je voulais utiliser semble faire planter FireFox/Iceweasel. Il serait donc intéressant d'utiliser un outil de capture qui scrolle automatiquement le contenu du navigateur pour récupérer tout le contenu de la page à coup sûr.
chown www-data.www-data /var/www su - www-data -c "vnc4server :1 -geometry 1280x6000 -depth 24" sleep 5 killall xterm
et pour le démarrage automatique du serveur VNC/X il faut ajouter les lignes suivante dans le fichier
/etc/rc.local
:su - www-data -c "vnc4server :1 -geometry 1280x6000 -depth 24" killall xterm
Vous pouvez maintenant vous connecter en VNC au serveur X de chaque containeur depuis un client VNC graphique (ex: Remmina) en indiquant l'adresse :
10.0.0.101:1
(ne pas oublier le :1 sinon le port n'est pas le bon)Pour régler le navigateur à la bonne position et éventuellement installer des add-ons, il faut le lancer manuellement dans l'un des containeurs :
su - www-data -c "firefox --display :1"
mettez Firefox en plein écran, effacez son cache et fermez l'application.
Nous allons sauver l'état de firefox pour avoir une version toujours stable et sans cache :
cp -Rp ~www-data/.mozilla ~www-data/.mozilla_reference
de même pour modifier, à la demande, les IP associées aux hostnames nous allons copier le fichiers /etc/hosts et permettre aux scripts Apache de pouvoir écrire ce fichier.
chown www-data /etc/hosts cp /etc/hosts /etc/hosts_reference
Scripts de capture unitaire
Toujours dans l'environnement de notre containeur (ou machine virtuelle) !
Le scritp Apache de capture CGI screenshot.cgi est à placer dans
/usr/lib/cgi-bin
du containeur est le suivant :#!/bin/sh # Fichier de verrou pour la section critique LOCK_FILE="/tmp/screenshot.lock" # Delai d'attente, en seconde, du rendu avant la capture DELAI_RENDU=20 # Indispensable pour la commande import export HOME=/var/www # Extraction des parametres CGI URL=`echo "$QUERY_STRING" | sed -n 's/^.*url=\([^&]*\).*$/\1/p'` IP=`echo "$QUERY_STRING" | sed -n 's/^.*ip=\([^&]*\).*$/\1/p'` # Decodage de l'URL URL=`echo "$URL" | sed -e 's/%\\([0-9A-F][0-9A-F]\\)/\\\\\\\\\\x\\1/g' | xargs echo -e ` #----------------------------------------------------------------------------- # Section critique exec 200>$LOCK_FILE flock -x 200 # Extraction du hostname depuis l'URL HOST="$URL" HOST="${HOST##http://}" HOST="${HOST##https://}" HOST="${HOST%%/*}" # Association IP<->HOST (si une ip a ete donnee) if [ "x$IP" != "x" ] ; then echo "" >> /etc/hosts echo "$IP $HOST" >> /etc/hosts fi # Fichier d'image unique IMAGE_FILE=`mktemp /tmp/screenshot.XXXXXXXXXX` # Reinitialisation de la configuration utilisateur de firefox a partir de celui de reference rm -rf /var/www/.mozilla cp -Rp /var/www/.mozilla_reference /var/www/.mozilla rm -rf /var/www/.mozilla/firefox/*.default/Cache/* # Execution de firefox, capture d'ecran du display concerne et arret de firefox /usr/bin/firefox --display :1 "$URL" > /dev/null 2>&1 & PID_FIREFOX=$! /bin/sleep $DELAI_RENDU /usr/bin/import -window root -display :1 -annotate +10+5960 "$URL" -annotate +10+5940 "$IP" -annotate +10+5920 "`date`" "png:$IMAGE_FILE" > /dev/null 2>&1 killall firefox-bin /bin/sleep 2 # Remise au propre des hostname if [ "x$IP" != "x" ] ; then cp /etc/hosts_reference /etc/hosts fi # fin de la section critique flock -u 200 #----------------------------------------------------------------------------- # Renvoi de l'image capturee au navigateur du client echo "Content-type: image/png" echo cat $IMAGE_FILE exit 0
Ce script doit être rendu exécutable pour l'utilisateur
www-data
(utilisateur sous lequel tourne Apache) :chown www-data.www-data /usr/lib/cgi-bin/screenshot.cgi chmod u+x /usr/lib/cgi-bin/screenshot.cgi
Une petite page web
screenshot.htm
pour tester le script à placer dans/var/www
:Capture-rendu image de page web
<form action=/cgi-bin/screenshot.cgi> <table> <tr><td>URL<td><input type=text name=url size=100> <tr><td>IP serveur (optionnel)<td><input type=text name=ip size=20> <tr><td><td><input type=submit value="Capturer"> </table> </form>
Il ne reste plus qu'à faire un test en ouvrant la page
http://10.0.0.101/screenshot.htm
et en saisissant une URL :et le résultat est :
Avec les annotations en bas d'image (url, ip (s'il y a), date de capture).
Distribution des serveurs de capture
Note: si vous avez utiliser des machines virtuelles (KVM, XEN ...) c'est à vous de dupliquer ces serveurs.
Notez que jusqu'à maintenant la notion de containeur ne nous a pas vraiment servi. Vous pouvez d'ailleurs installer ce système de capture sur un simple serveur en suivant les manipulations précédentes en dehors de tout système de machine virtuelle.
Nous allons maintenant cloner notre containeur pour avoir plusieurs serveurs de rendu sur la même machine physique.
Le clonage des containeurs se fait simplement par dump du 1er containeur depuis le serveur maître :
vzdump --compress --stop 101
puis
ls -l /var/lib/vz/dump/
pour retrouver le nom du fichier créée par le dump (dans mon cas : vzdump-openvz-101-2011_07_18-22_26_01.tgz)
et enfin
vzrestore /var/lib/vz/dump/vzdump-openvz-101-2011_07_18-22_26_01.tgz 102
il faut maintenant changer le hostname et l'IP du containeur :
vzctl set 102 --hostname screenshoter-102.bogus.fr --save vzctl set 102 --ipdel 10.0.0.101 --savevzctl set 102 --ipadd 10.0.0.102 --save
il ne reste plus qu'à démarrer le nouveau containeur
vzctl start 102
et un petit test :
http://10.0.0.102/screenshot.htm
Voilà, il faut répéter l'opération autant de fois que nécessaire, disons jusqu'au containeur 105 pour avoir 5 captures de page web sur la même machine physique.
Il est possible d'utiliser plusieurs serveurs physiques en y copiant le dump créé par la commande vzdump.
Mutualisation des serveurs de capture
Maintenant, sur un serveur web indépendant, qui peut être sur la machine maître en dehors des containeurs ou alors dans l'un des containeurs, nous allons utiliser Apache pour y placer des scripts de mutualisation.
Nous allons utiliser un script en PHP, il faut donc installer le module Apache correspondant :
apt-get install libapache2-php5
Le script de rotation automatique des serveurs/containeurs est :
<?php ===================================== // Tableau des serveurs/containeurs de capture $screenshoters = array( "10.0.0.101", "10.0.0.102", "10.0.0.103", "10.0.0.104", "10.0.0.105" ); // Le fichier /tmp/screenshot_num.txt (créé lors du premier appel) contient l'indice du dernier serveur/containeur utilisé $fp = fopen("/tmp/screenshot_num.txt", "c+"); // Section critique if (flock($fp, LOCK_EX)) { if (($num = fgets($fp)) === FALSE) $num = 0; $num = trim($num); // Incrément du numéro de serveur module le nombre maximum de serveurs/containeurs $num = ((int)$num + 1) % count($screenshoters); ftruncate($fp, 0); fputs($fp, $num); flock($fp, LOCK_UN); } // Redirection vers le serveur/containeur 'choisi' $url=urlencode($_REQUEST['url']); $ip=urlencode($_REQUEST['ip']); $redirect = "http://$screenshoters[$num]/cgi-bin/screenshot.cgi?url=$url&ip=$ip"; header("Location: $redirect"); ?>
Il suffit de l'utiliser avec une url de la forme
http://10.0.0.1/screenshot.php?url=url_encodee&ip=ip_serveur_de_capture
.Le script renvoie chaque demande vers l'un des serveurs de capture par simple rotation des adresses IP des serveurs de rendu.
Il est aussi possible d'utiliser une autre solution pour la distribution des serveurs, comme par exemple un reverse cache (varnish, nginx, ...).
Monitoring du rendu sur des serveurs load balancés
Voici un dernier script PHP, à placer dans
/var/www/screenshot_wall.php
, qui permet d'afficher côte-à-côte le rendu d'une même url sur plusieurs frontaux web :<?php $ips = $_REQUEST[ip]; $url = urlencode($_REQUEST[url]); if (!is_array($_REQUEST[ip])) $ips = explode("\r\n", $_REQUEST[ip]); if (isset($_REQUEST[refresh])) { print "<head>\n"; print " <meta http-equiv='refresh' content='$_REQUEST[refresh]'>\n"; print "</head>\n"; } print "<h3 align=center>$_REQUEST[url]</h3>\n"; print "<h4 align=center>" . date('r') . "</h4>\n"; foreach ($ips as $ip) { $ip = trim($ip); if (!empty($ip)) print " <a href='/screenshot.php?url=$url&ip=$ip' target='shot_$_REQUEST[url]_$ip'><img width=$_REQUEST[width] height=$_REQUEST[height] src='/screenshot.php?url=$url&ip=$ip' title=$ip></a>\n"; } ?> <hr> <form action=screenshot_wall.php> <table border=0> <tr><td align=right>URL : <td><input type=text name=url size=100 value="http://www.leguide.com"> <tr><td align=right>IP : <td><textarea name=ip rows=10 cols=20> 192.168.0.60 192.168.0.43 192.168.0.41 192.168.0.40 192.168.0.152 192.168.0.123 192.168.0.47 192.168.0.48 </textarea> <tr><td align=right>L x H : <td><input type=text name=width size=5 value=160>x<input type=text name=height size=5 value=1000> <tr><td align=right>Refresh : <td><input type=text name=refresh value=1200>http://screenshot_wall.ph <tr><td><input type=submit valus="Envoyer"> </table> </form>
et à tester en appelant
http://10.0.0.1/screenshot_wall.php
.et voici une version en HTML dynamique qui réduit la charge vers les containeurs en raffraichissant les images une par une :
<html> <head> </head> <body> <script type="text/javascript"> var image_num = 0; var image_count = 5; var inter_image_delai = 15000; var ip; var url = ""; var refresh_delai = 30000; function refresh_suivante() { if (image_num >= image_count) { image_num = 0; setTimeout("refresh_suivante()", refresh_delai); } else { image = document.images[image_num]; if (image.src.indexOf("/ajax-loader.gif") == -1) { image.src = "/ajax-loader.gif"; setTimeout("refresh_suivante()", 2000); } else { image.src = "/screenshot.php?url=" + escape(url) + "&ip=" + ip[image_num]; setTimeout("refresh_suivante()", inter_image_delai); image_num = image_num + 1; } } } function go() { ip = document.forms[0].ip.value.split('\n'); url = document.forms[0].url.value; width = document.forms[0].width.value;================================================== height = document.forms[0].height.value; refresh_delai = document.forms[0].refresh.value * 1000; image_count = 0; var image_tags = ""; for (i = 0; i < ip.length; i++) { if (ip[i] != "") { image_tags = image_tags + "<a href='/screenshot.php?url=" + escape(url) + "&ip=" + ip[i] + "' target='shot_" + escape(url) + "_" + ip[i] + "'><img width=" + width + " height=" + height + " title=" + ip[i] + "></a>\n"; image_count++; } } document.getElementById("images").innerHTML = image_tags; setTimeout("refresh_suivante()", 1000); } </script> <div id="images"> </div> <hr> <form action="javascript:go()"> <table border=0> <tr><td align=right>URL : <td><input type=text name=url size=100 value="http://www.leguide.com"> <tr><td align=right>IP : <td><textarea name=ip rows=10 cols=20> 192.168.0.60 192.168.0.43 192.168.0.41 192.168.0.40 192.168.0.152 192.168.0.123 192.168.0.47 192.168.0.48 </textarea> <tr><td align=right>L x H : <td><input type=text name=width size=5 value=160>x<input type=text name=height size=5 value=800> <tr><td align=right>Refresh : <td><input type=text name=refresh value=30> <tr><td><input type=submit value="Envoyer"> </table> </form> </body> </html>
Améliorations possibles
Il y a pas mal d'améliorations possibles :
- sécuriser le système : vérification des paramètres des scripts (IP, url ...), sécurisation des navigateurs (javascript ...) , sécurition des OS virtuels (réinstallation automatique par image sûre ...).
- choix du navigateur à chaque rendu (Chrome, Opera, IE* ...)
- choix de la résolution du navigateur
- capture avec scrolling de la page web
- afficher temps et/ou graphe de chargement de la page web
- cache des images avec épuration automatique
A bientôt
Antoine