Serveur de capture de page web
par , le lundi 11 juillet 2011 à 11:53

Catégorie : Général
Mots clés :

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 paralèlle 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 controlera 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 distributer 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).

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 machines virtuelles (appelées 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)

Configuration des outils pour la capture des pages web :

Attention ! nous entrons maintenant dans le containeur (ou la machine virtuelle) et la suite des commandes est à exécuter à l'intétieur 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 reprennant 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 :

 

<h3>Capture-rendu image de page web</h3>

<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 --save
vzctl 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 :

Ecrire à l'auteur