Artisan Numérique

/système/sécurité/bureau/ Un serveur HTTP à usage unique pour transferer vos fichiers

Lorsque l'on a besoin d'envoyer juste un fichier, genre une photo, une archive, un bon gros document, c'est toujours la même galère. D'abord on tente de passer par la messagerie instantanée mais bien évidemment les deux routeurs ne sont pas configurés pour laisser passer le port 42654 en UDP subsonique qui va bien, et ça démarre... jamais. Alors on se rabat sur ce bon vieux mail, et hop, un attachement... et là c'est le serveur SMTP du copain qui nous vomi notre fichier car la belle Alice a des quotas bien en deçà de son tour de poitrine. Là, pris d'un coup de blues, on se dit qu'un petit coup de scp serait bien pratique mais le client d'en face n'a point de serveur SSH, d'ailleurs il est sous Windows et ne sait même pas ce que c'est, SSH. Et cela fini toujours invariablement de la même manière, copié sur son site WEB, à la racine comme un gros porcasse, et renommé à la hâte toto.zip, pour être sur que le gars d'en face ne se goure pas dans l'URL. Le temps passe et la dite racine devient un authentique dépotoir.

Et aujourd'hui, alors que je bidouillais benoîtement le paquet Perl HTTP::Server::Simple, voilà justement qu'un ami me demande de lui envoyer un PDF sur Drupal que je lui avais promis depuis 12 lustres et qui traînait dans un coin de ma machine. Je recommence le périple sus-cité, GTalk ne prends pas les downloads, cool... Puis je me demande ce que cela donnerait de bidouiller rapidement le fichier d'exemple que j'étais en train d'étudier pour envoyer ce maudit fichier, tout connement par le canal HTTP. Et bien mes amis, ça marche vraiment très bien. Un peu d'huile de coude et juste 58 lignes de code plus tard, me voilà doté d'un magnifique serveur WEB en ligne de commande et à usage unique.

Le squelette de Nestor

Le principe est simple, le serveur que nous allons fabriquer va juste être invoqué en ligne de commande avec le fichier à servir en paramètre, se mettre en attente de connexion, injecter le fichier et s'arrêter. Simple et sans aucune faille de sécurité possible.

Pour faire cela nous allons utiliser HTTP::Server::Simple, dont j'ai simplement un peu modifié le squelette donné en exemple :

#! /usr/bin/perl
my ($file)=@ARGV;
my $progress;
open($progress,"| zenity --progress --auto-close --auto-kill --title=\"Nestor 1.0\" --text=\"Initialisation...\" ; kill -9 $$");
$progress->autoflush(1);

# définition du serveur HTTP
{
   package Nestor;

  use HTTP::Server::Simple::CGI;
  use base qw(HTTP::Server::Simple::CGI);
  use File::Basename;
  use MIME::Types qw(by_suffix);
  use URI::Escape;


  # Affichage du dialogue de progression
  sub print_banner {
      my ($self) = @_;
      print $progress "# En attente...\n";
  }

  # Ici seront traitées les requêtes
  sub handle_request {
  }
}

# Démarrage du serveur sur le port 8080
Nestor->new(8080)->run();
Le squelette de Nestor

Rien de sorcier jusqu'ici, il s'agit juste de surcharger la classe HTTP:Server::Simple en redéfinissant principalement la méthode print_banner qui est normalement utilisée pour afficher un message au démarrage du serveur. Ici nous détournons un peu son usage premier pour créer un tube ouvert sur zenity.

Pour ceux qui ne connaissent pas, Zenity est un outil gnome qui permet de dialoguer graphiquement avec l'utilisateur sans avoir à mettre le nez dans la programmation GTK. Ici nous l'utilisons en mode "barre de progression". En ouvrant ainsi un tube perl, nous allons pouvoir afficher des messages texte en les écrivant précédés par le caractère #. Et pour faire bouger la barre de progression ce sont des valeurs de 0 à 100 qu'il faut injecter. --auto-close indique à zenity de se fermer automatiquement lorsqu'il arrive à 100%.

Dans un cas normal nous aurions pu utiliser l'option --auto-kill qui consiste à envoyer un signal SIGHUP au processus parent de zenity lorsque l'utilisateur presse le bouton annuler. Le problème est que notre serveur est déjà un processus enfant de celui qui lance zenity, donc ça ne marche pas super. Et ceci d'autant plus que le dit serveur détourne SIGUP à son usage.

Bref, l'option choisie ici est une belle bidouille de la famille "désespoir de cause" consistant à rajouter la mise à mort par kill du processus parent au moment où zenity s'arrête, ce qui correspond à celui où l'utilisateur presse cancel. La commande kill va elle prendre en charge la destruction des processus enfant et donc du serveur.

Prise en charge des requêtes

Là c'est la partie "fun" qui commence, avec la fameuse méthode qui gère nos requêtes, handle_request :

sub handle_request {
  my ($self,$cgi) = @_;

  my $url="/".basename($file);
  if ($url eq $cgi->path_info()) {
    print $progress "# Téléchargement en cours...\n";

    my ($content_type, undef) = by_suffix($file);
    my $content_length= -s "$file";
    print "HTTP/1.0 200 OK\n";
    print $cgi->header(
      -type => $content_type,
      -disposition => "attachement;filename=".uri_escape($url),
      -length      => $content_length,
            );
    open( FILE, "<$file" );
    my $progress=0;
    while(read(FILE, my $buffer, 1024) ){
      $progress+=1024;
      print $progress (100*($progress/$content_length))."\n";
      print $buffer;
    }
    exit();
  } else {
    print "HTTP/1.0 302 Found\n";
    print $cgi->redirect($url);
  }
}
Méthode handle_request

Pour éviter d'avoir à taper une URL longuettes, nous allons faire appel à une petite astuce. Lorsque l'utilisateur va taper l'URL http://ma_machine:8080/, nous allons lui renvoyer un code HTTP 302 qui veut dire, "la ressource existe bien, mais elle se trouve ailleurs". Le "ailleurs" en question est spécifié par le champ du header location qui contient le nom du fichier encodé en tant qu'URL. Techniquement nous aurions pu envoyer directement le fichier mais l'expérience montre que les navigateurs aiment bien que le même nom du fichier à téléchargé soit le même que celui de l'URL.

La navigateur va donc interpréter le champ location et refaire directement une seconde requête, avec le bon chemin cette fois. Là, nous recherchons le type mime du fichier à envoyer et sa taille, puis nous fabriquons une en-tête de téléchargement tout ce qu'il y a de plus classique. Après il suffit de lire fichier par block de 1024 octets et de l'envoyer sur le réseau. Une fois ce travail effectué, un coup de exit() met le serveur à mort, sa mission accomplie.

Notez que la mise à jour de la barre de progression se fait dans la boucle de lecture de fichier, cela nous permet de voir où en est le téléchargement.

Maintenant nestor est utilisable en local (http://mon_ip_local:8080), et pour l'être de l'extérieur il va falloir trouer un peu le routeur en redirigeant les connexions entrante sur le port 8080 vers votre machine de travail. Ceci fait, vous pouvez tester sur http://mon_ip_publique:8080.

Conclusion

Pour ceux que cela intéresse, vous pouvez directement télécharger la version complète de nestor qui va un peu plus loin que le tutoriel en ajoutant la prise en charge des dossiers et du mode "texte" lorsque lancé en ligne de commande. Pour cela, utilisez le lien à la fin de cet article.

Cette version contient aussi un peu de code pour permettre d'utiliser le script par un simple click-droit dans Nautilus. Pour y parvenir, il suffit de le sauvegarder dans votre dossier ~/.gnome2/nautilus_scripts et de le rendre exécutable.


Le père nestor