CONTENU

  • NOM
  • DESCRIPTION
  • CONCEPTS
  • UTILISATION DES FILTRES
  • ÉCRITURE D'UN FILTRE SOURCE
  • ÉCRITURE D'UN FILTRE SOURCE EN C
  • CRÉATION D'UN FILTRE SOURCE EN TANT QU'EXÉCUTABLE SÉPARÉ
  • ÉCRIRE UN FILTRE SOURCE EN PERL
  • UTILISATION DU CONTEXTE : LE FILTRE DE DÉBOGAGE
  • CONCLUSION
  • LIMITATIONS
  • POINTS A SURVEILLER
  • EXIGENCES
  • AUTEUR
  • Droits d'auteur

NOM

perlfilter - Filtres de source

DESCRIPTION

Cet article traite d'une fonctionnalité peu connue de Perl appelée filtres de source. Les filtres de source modifient le texte du programme d'un module avant que Perl ne le voie, un peu comme un préprocesseur C modifie le texte source d'un programme C avant que le compilateur ne le voie. Cet article vous en dit plus sur ce que sont les filtres de source, comment ils fonctionnent, et comment écrire les vôtres.

Le but initial des filtres de source était de vous permettre de crypter la source de votre programme pour empêcher le piratage occasionnel. Ce n'est pas tout ce qu'ils peuvent faire, comme vous l'apprendrez bientôt. Mais d'abord, les bases.

CONCEPTS

Avant que l'interpréteur Perl puisse exécuter un script Perl, il doit d'abord le lire d'un fichier en mémoire pour l'analyser et le compiler. Si ce script comprend lui-même d'autres scripts avec un code use ou require alors chacun de ces scripts devra également être lu depuis leurs fichiers respectifs.

Maintenant, pensez à chaque connexion logique entre l'analyseur Perl et un fichier individuel comme un... flux source. Un flux source est créé lorsque l'analyseur Perl ouvre un fichier, il continue d'exister lorsque le code source est lu en mémoire, et il est détruit lorsque Perl a fini d'analyser le fichier. Si l'analyseur syntaxique rencontre un require ou use dans un flux source, un nouveau flux distinct est créé juste pour ce fichier.

Le diagramme ci-dessous représente un seul flux de source, avec le flux de source d'un fichier de script Perl à gauche dans l'analyseur Perl à droite. C'est ainsi que Perl fonctionne normalement.

file -------> parser

Il y a deux points importants à retenir :

  1. Bien qu'il puisse y avoir un nombre quelconque de flux source en existence à un moment donné, un seul sera actif.

  2. Chaque flux source est associé à un seul fichier.

Un filtre de source est un type spécial de module Perl qui intercepte et modifie un flux source avant qu'il n'atteigne l'analyseur syntaxique. Un filtre de source modifie notre diagramme comme ceci :

file ----> filter ----> parser

Si cela n'a pas beaucoup de sens, considérez l'analogie d'un pipeline de commande. Disons que vous avez un script shell stocké dans le fichier compressé. trial.gz. La commande pipeline simple ci-dessous exécute le script sans avoir besoin de créer un fichier temporaire pour contenir le fichier non compressé.

gunzip -c trial.gz | sh

Dans ce cas, le flux de données du pipeline peut être représenté comme suit :

trial.gz ----> gunzip ----> sh

Avec les filtres de source, vous pouvez stocker le texte de votre script compressé et utiliser un filtre de source pour le décompresser pour le parseur de Perl :

 compressed           gunzip
Perl program ---> source filter ---> parser

UTILISATION DE FILTRES

Alors comment utiliser un filtre de source dans un script Perl ? Plus haut, j'ai dit qu'un filtre de source est juste un type spécial de module. Comme tous les modules Perl, un filtre source est invoqué avec une instruction use.

Disons que vous voulez faire passer votre source Perl par le préprocesseur C avant son exécution. Il se trouve que la distribution des filtres de source est livrée avec un module de filtre de préprocesseur C appelé Filter::cpp.

Voici un programme d'exemple, cpp_test, qui fait usage de ce filtre. Les numéros de ligne ont été ajoutés pour permettre de référencer facilement des lignes spécifiques.

1:use Filter::cpp;2:#define TRUE 13:$a= TRUE;4:print"a = $an";

Lorsque vous exécutez ce script, Perl crée un flux source pour le fichier. Avant que l'analyseur syntaxique ne traite une des lignes du fichier, le flux source ressemble à ceci :

cpp_test ---------> parser

Ligne 1, use Filter::cpp, inclut et installe le fichier cpp module de filtrage. Tous les filtres de source fonctionnent de cette manière. L'instruction use est compilée et exécutée au moment de la compilation, avant toute autre lecture du fichier, et elle attache le filtre cpp au flux source en coulisse. Maintenant, le flux de données ressemble à ceci :

cpp_test ----> cpp filter ----> parser

Lorsque l'analyseur syntaxique lit la deuxième ligne et les lignes suivantes du flux source, il fait passer ces lignes par le filtre cpp source filter avant de les traiter. Le site cpp passe simplement chaque ligne à travers le préprocesseur C réel. La sortie du préprocesseur C est ensuite réinsérée dans le flux source par le filtre.

.-> cpp --.|||||<-'
cpp_test ----> cpp filter ----> parser

L'analyseur syntaxique voit alors le code suivant :

use Filter::cpp;$a=1;print"a = $an";

Considérons ce qui se passe lorsque le code filtré inclut un autre module avec utilisation :

1:use Filter::cpp;2:#define TRUE 13:use Fred;4:$a= TRUE;5:print"a = $an";

Le site cpp ne s'applique pas au texte du module Fred, mais seulement au texte du fichier qui l'a utilisé (cpp_test). Bien que l'instruction use de la ligne 3 passe à travers le filtre cpp, le module qui est inclus (Fred) ne le feront pas. Les flux de sources ressemblent à ceci après que la ligne 3 ait été analysée et avant que la ligne 4 soit analysée :

cpp_test ---> cpp filter ---> parser (INACTIVE)

Fred.pm ----> parser

Comme vous pouvez le voir, un nouveau flux a été créé pour la lecture de la source à partir de Fred.pm. Ce flux restera actif jusqu'à ce que la totalité de Fred.pm ait été analysé. Le flux source de cpp_test existera toujours, mais sera inactif. Une fois que l'analyseur syntaxique a fini de lire Fred.pm, le flux source qui lui est associé sera détruit. Le flux source de cpp_test redevient alors actif et l'analyseur syntaxique lit la ligne 4 et les lignes subséquentes du fichier cpp_test.

Vous pouvez utiliser plus d'un filtre source sur un même fichier. De même, vous pouvez réutiliser le même filtre dans autant de fichiers que vous le souhaitez.

Par exemple, si vous avez un fichier source uuencodé et compressé, il est possible d'empiler un filtre uuencodé et un filtre de décompression comme ceci :

use Filter::uudecode;use Filter::uncompress;
M'XL(".H'V9I;F%L')Q;>7/;1I;_>_I3=&E=%:F*I"T?22Q/
M6]9*<IQCO*XFT"0[PL%%'Y+IG?WN^ZYN-$'J.[.JE$,20/?K=_[>...

Une fois que la première ligne a été traitée, le flux ressemblera à ceci :

file ---> uudecode ---> uncompress ---> parser
           filter         filter

Les données passent par les filtres dans le même ordre qu'ils apparaissent dans le fichier source. Le filtre uudecode est apparu avant le filtre de décompression, donc le fichier source sera uudecodé avant d'être décompressé.

ÉCRITURE D'UN FILTRE SOURCE

Il y a trois façons d'écrire votre propre filtre source. Vous pouvez l'écrire en C, utiliser un programme externe comme filtre, ou écrire le filtre en Perl. Je ne couvrirai pas les deux premières en détail, donc je vais d'abord les évacuer. Écrire le filtre en Perl est le plus pratique, donc je lui consacrerai le plus d'espace.

ÉCRITURE D'UN FILTRE SOURCE EN C

La première des trois techniques disponibles consiste à écrire le filtre complètement en C. Le module externe que vous créez s'interface directement avec les crochets du filtre source fournis par Perl.

L'avantage de cette technique est que vous avez un contrôle complet sur l'implémentation de votre filtre. Le gros inconvénient est la complexité accrue requise pour écrire le filtre - non seulement vous devez comprendre les crochets du filtre source, mais vous devez également avoir une connaissance raisonnable des tripes de Perl. L'une des rares fois où cela vaut la peine de se donner cette peine est pour écrire un brouilleur de sources. Le site decrypt (qui désembrouille la source avant que Perl ne l'analyse) inclus dans la distribution des filtres de source est un exemple de filtre de source C (voir Filtres de décryptage, ci-dessous).

Filtres de décryptage

Tous les filtres de décryptage fonctionnent sur le principe de la "sécurité par l'obscurité". Indépendamment de la façon dont vous écrivez un filtre de décryptage et de la force de votre algorithme de cryptage, toute personne suffisamment déterminée peut récupérer le code source original. La raison en est très simple : une fois que le filtre de décryptage a ramené la source à sa forme originale, des fragments de celle-ci seront stockés dans la mémoire de l'ordinateur au fur et à mesure que Perl l'analyse. La source pourrait n'être en mémoire que pendant une courte période, mais toute personne possédant un débogueur, des compétences et beaucoup de patience peut éventuellement reconstruire votre programme.

Cela dit, il existe un certain nombre de mesures qui peuvent être prises pour rendre la vie difficile au craqueur potentiel. La plus importante : écrire votre filtre de décryptage en C et lier statiquement le module de décryptage au binaire Perl. Pour d'autres conseils pour rendre la vie difficile au pirate potentiel, voir le fichier decrypt.pm dans la distribution des filtres sources.

CRÉATION D'UN FILTRE SOURCE EN TANT QU'EXÉCUTABLE SÉPARÉ.

Une alternative à l'écriture du filtre en C est de créer un exécutable séparé dans le langage de votre choix. L'exécutable séparé lit à partir de l'entrée standard, effectue tout traitement nécessaire et écrit les données filtrées sur la sortie standard. Filter::cpp est un exemple de filtre source implémenté comme un exécutable séparé - l'exécutable est le préprocesseur C fourni avec votre compilateur C.

La distribution du filtre source comprend deux modules qui simplifient cette tâche : Filter::exec et Filter::sh. Les deux vous permettent d'exécuter n'importe quel exécutable externe. Les deux utilisent un coprocessus pour contrôler le flux de données entrant et sortant de l'exécutable externe. (Pour plus de détails sur les coprocessus, voir Stephens, W.R., "Advanced Programming in the UNIX Environment". Addison-Wesley, ISBN 0-210-56317-7, pages 441-445). La différence entre eux est que Filter::exec génère directement la commande externe, tandis que Filter::sh génère un shell pour exécuter la commande externe. (Unix utilise l'interpréteur de commandes Bourne ; NT utilise l'interpréteur de commandes cmd.) Le lancement d'un interpréteur de commandes vous permet d'utiliser les métacaractères de l'interpréteur de commandes et les facilités de redirection.

Voici un exemple de script qui utilise Filter::sh:

use Filter::sh 'tr XYZ PQR';$a=1;print"XYZ a = $an";

La sortie que vous obtiendrez lorsque le script sera exécuté :

PQR a =1

L'écriture d'un filtre source en tant qu'exécutable séparé fonctionne bien, mais une petite pénalité de performance est encourue. Par exemple, si vous exécutez le petit exemple ci-dessus, un sous-processus séparé sera créé pour exécuter le programme Unix. tr commande. Chaque utilisation du filtre nécessite son propre sous-processus. Si la création de sous-processus est coûteuse sur votre système, vous pouvez envisager l'une des autres options de création de filtres source.

ÉCRIRE UN FILTRE SOURCE EN PERL

L'option la plus facile et la plus portable disponible pour créer votre propre filtre source est de l'écrire complètement en Perl. Pour distinguer cette technique des deux précédentes, je l'appellerai un filtre source en Perl.

Pour aider à comprendre comment écrire un filtre source Perl, nous avons besoin d'un exemple à étudier. Voici un filtre source complet qui effectue le décodage de rot13. (Rot13 est un schéma de cryptage très simple utilisé dans les messages Usenet pour cacher le contenu des messages offensifs. Il avance chaque lettre de treize places, de sorte que A devient N, B devient O, et Z devient M).

package Rot13;use Filter::Util::Call;sub import{my($type)=@_;my($ref)=[];
   filter_add(bless $ref);}sub filter{my($self)=@_;my($status);tr/n-za-mN-ZA-M/a-zA-Z/if($status= filter_read())>0;$status;}1;

Tous les filtres sources Perl sont implémentés comme des classes Perl et ont la même structure de base que l'exemple ci-dessus.

Tout d'abord, nous incluons le Filter::Util::Call qui exporte un certain nombre de fonctions dans l'espace de noms de votre filtre. Le filtre présenté ci-dessus utilise deux de ces fonctions, filter_add() et filter_read().

Ensuite, nous créons l'objet filtre et l'associons au flux source en définissant la fonction import . Si vous connaissez assez bien Perl, vous savez que la fonction import est appelée automatiquement chaque fois qu'un module est inclus dans une instruction use. Cela rend import l'endroit idéal pour à la fois créer et installer un objet filtre.

Dans l'exemple de filtre, l'objet ($ref) est béni comme n'importe quel autre objet Perl. Notre exemple utilise un tableau anonyme, mais ce n'est pas une obligation. Comme cet exemple n'a pas besoin de stocker des informations de contexte, nous aurions pu utiliser une référence scalaire ou de hachage tout aussi bien. La section suivante démontre les données de contexte.

L'association entre l'objet de filtrage et le flux source est faite avec l'attribut filter_add() . Celle-ci prend un objet filtre comme paramètre ($ref dans ce cas) et l'installe dans le flux source.

Enfin, il y a le code qui effectue réellement le filtrage. Pour ce type de filtre source Perl, tout le filtrage est effectué dans une méthode appelée filter(). (Il est également possible d'écrire un filtre source Perl en utilisant une fermeture. Voir la méthode

page de manuel pour plus de détails). Il est appelé chaque fois que l'analyseur Perl a besoin d'une autre ligne de source à traiter. Le site filter() lit, à son tour, les lignes du flux source en utilisant la méthode filter_read() fonction.

Si une ligne était disponible à partir du flux source, filter_read() renvoie une valeur d'état supérieure à zéro et ajoute la ligne à la section $_. Une valeur de statut de zéro indique la fin du fichier, moins de zéro signifie une erreur. La fonction de filtrage elle-même est censée retourner son statut de la même manière, et mettre la ligne filtrée qu'elle veut écrire dans le flux source dans $_. L'utilisation de $_ explique la brièveté de la plupart des filtres de source Perl.

Afin d'utiliser le filtre rot13, nous avons besoin d'un moyen d'encoder le fichier source au format rot13. Le script ci-dessous, mkrot13, fait exactement cela.

die"usage mkrot13 filenamen"unless@ARGV;my$in=$ARGV[0];my$out="$in.tmp";
open(IN,"<$in")ordie"Cannot open file $in: $!n";
open(OUT,">$out")ordie"Cannot open file $out: $!n";print OUT "use Rot13;n";while(){tr/a-zA-Z/n-za-mN-ZA-M/;print OUT;}

close IN;
close OUT;
unlink $in;
rename $out,$in;

Si nous cryptons ceci avec mkrot13:

print" hello fred n";

le résultat sera le suivant :

use Rot13;
cevag "uryyb serqa";

En l'exécutant, on obtient cette sortie :

hello fred

EN UTILISANT LE CONTEXTE : LE FILTRE DE DÉBOGAGE

L'exemple de rot13 était un exemple trivial. Voici une autre démonstration qui montre un peu plus de fonctionnalités.

Disons que vous vouliez inclure beaucoup de code de débogage dans votre script Perl pendant le développement, mais que vous ne vouliez pas qu'il soit disponible dans le produit publié. Les filtres de source offrent une solution. Afin de garder l'exemple simple, disons que vous vouliez que la sortie de débogage soit contrôlée par une variable d'environnement, DEBUG. Le code de débogage est activé si la variable existe, sinon il est désactivé.

Deux lignes de marqueurs spéciaux mettront le code de débogage entre parenthèses, comme ceci :

## DEBUG_BEGINif($year>1999){
   warn "Debug: millennium bug in year $yearn";}## DEBUG_END

Le filtre garantit que Perl analyse le code entre les lignes de marquage et DEBUG_END uniquement lorsque la balise DEBUG existe. Cela signifie que lorsque DEBUG existe, le code ci-dessus doit être transmis au filtre sans modification. Les lignes de marquage peuvent également être passées telles quelles, car l'analyseur Perl les verra comme des lignes de commentaire. Lorsque DEBUG n'est pas défini, nous devons trouver un moyen de désactiver le code de débogage. Un moyen simple d'y parvenir est de convertir les lignes entre les deux marqueurs en commentaires :

## DEBUG_BEGIN#if ($year > 1999) {#     warn "Debug: millennium bug in year $yearn";#}## DEBUG_END

Voici le filtre Debug complet :

package Debug;use strict;use warnings;use Filter::Util::Call;use constant TRUE =>1;use constant FALSE =>0;sub import{my($type)=@_;my(%context)=(
     Enabled => defined $ENV{DEBUG},
     InTraceBlock => FALSE,
     Filename =>(caller)[1],
     LineNo =>0,
     LastBegin =>0,);
   filter_add(bless %context);}sub Die{my($self)= shift;my($message)= shift;my($line_no)= shift ||$self->{LastBegin};die"$message at $self->{Filename} line $line_no.n"}sub filter{my($self)=@_;my($status);$status= filter_read();++$self->{LineNo};# deal with EOF/error firstif($status<=0){$self->Die("DEBUG_BEGIN has no DEBUG_END")if$self->{InTraceBlock};return$status;}if($self->{InTraceBlock}){if(/^s*##s*DEBUG_BEGIN/){$self->Die("Nested DEBUG_BEGIN",$self->{LineNo})}elsif(/^s*##s*DEBUG_END/){$self->{InTraceBlock}= FALSE;}# comment out the debug lines when the filter is disableds/^/#/if!$self->{Enabled};}elsif(/^s*##s*DEBUG_BEGIN/){$self->{InTraceBlock}= TRUE;$self->{LastBegin}=$self->{LineNo};}elsif(/^s*##s*DEBUG_END/){$self->Die("DEBUG_END has no DEBUG_BEGIN",$self->{LineNo});}return$status;}1;

La grande différence entre ce filtre et l'exemple précédent est l'utilisation de données contextuelles dans l'objet filtre. L'objet filtre est basé sur une référence de hachage, et est utilisé pour conserver divers éléments d'information contextuelle entre les appels à la fonction filtre. Tous les champs de hachage, sauf deux, sont utilisés pour le signalement des erreurs. Le premier de ces deux champs, Enabled, est utilisé par le filtre pour déterminer si le code de débogage doit être transmis à l'analyseur Perl. Le second, InTraceBlock, est vrai lorsque le filtre a rencontré une erreur de type DEBUG_BEGIN mais n'a pas encore rencontré la ligne suivante DEBUG_END ligne.

Si vous ignorez toutes les vérifications d'erreurs que la plupart du code effectue, l'essence du filtre est la suivante :

sub filter{my($self)=@_;my($status);$status= filter_read();# deal with EOF/error firstreturn$statusif$status<=0;if($self->{InTraceBlock}){if(/^s*##s*DEBUG_END/){$self->{InTraceBlock}= FALSE
      }# comment out debug lines when the filter is disableds/^/#/if!$self->{Enabled};}elsif(/^s*##s*DEBUG_BEGIN/){$self->{InTraceBlock}= TRUE;}return$status;}

Soyez avertis : tout comme le préprocesseur C ne connaît pas le C, le filtre Debug ne connaît pas le Perl. Il peut être trompé assez facilement :

print<<EOM;##DEBUG_BEGIN
EOM

Mis à part cela, vous pouvez voir que l'on peut réaliser beaucoup de choses avec une quantité modeste de code.

CONCLUSION

Vous avez maintenant une meilleure compréhension de ce qu'est un filtre source, et vous pourriez même avoir une utilisation possible pour eux. Si vous avez envie de jouer avec les filtres source mais que vous avez besoin d'un peu d'inspiration, voici quelques fonctionnalités supplémentaires que vous pourriez ajouter au filtre Debug.

Tout d'abord, une fonction facile. Plutôt que d'avoir un code de débogage qui est tout ou rien, il serait beaucoup plus utile de pouvoir contrôler quels blocs spécifiques de code de débogage sont inclus. Essayez d'étendre la syntaxe des blocs de débogage pour permettre à chacun d'être identifié. Le contenu de la balise DEBUG peut alors être utilisé pour contrôler quels blocs sont inclus.

Une fois que vous pouvez identifier les blocs individuels, essayez de leur permettre d'être imbriqués. Ce n'est pas difficile non plus.

Voici une idée intéressante qui n'implique pas le filtre Debug. Actuellement, les sous-routines Perl ont un support assez limité pour les listes de paramètres formelles. Vous pouvez spécifier le nombre de paramètres et leur type, mais vous devez toujours les sortir manuellement de la liste des paramètres. @_ vous-même. Ecrivez un filtre source qui vous permet d'avoir une liste de paramètres nommée. Un tel filtre tournerait comme ceci :

sub MySub($first,$second,@rest){...}

en ceci :

sub MySub($$@){my($first)= shift;my($second)= shift;my(@rest)=@_;...}

Enfin, si vous avez envie d'un vrai défi, essayez d'écrire un préprocesseur de macro Perl complet en tant que filtre source. Empruntez les fonctionnalités utiles du préprocesseur C et de tout autre processeur de macro que vous connaissez. La partie délicate sera de choisir le degré de connaissance de la syntaxe de Perl que vous voulez que votre filtre ait.

LIMITATIONS

Les filtres de source ne fonctionnent qu'au niveau de la chaîne de caractères, donc sont très limités dans sa capacité à modifier le code source à la volée. Il ne peut pas détecter les commentaires, les chaînes citées, les heredocs, il n'est pas un remplacement pour un vrai parser. Le seul usage stable pour les filtres de source sont le cryptage, la compression, ou le byteloader, pour traduire le code binaire en code source.

Voir par exemple les limitations de Switch, qui utilise des filtres de source, et qui donc ne fonctionne pas à l'intérieur d'un eval de chaîne, la présence de regex avec des nouvelles lignes incorporées qui sont spécifiées avec des raw. /.../ bruts et qui n'ont pas de modificateur //x sont indiscernables des morceaux de code commençant par l'opérateur de division /. Comme solution de contournement, vous devez utiliser m/.../ ou m?...? pour de tels motifs. De même, la présence de regex spécifiées avec des motifs bruts ?...? bruts peut provoquer des erreurs mystérieuses. La solution de contournement consiste à utiliser m?...? à la place. Voir https://search.cpan.org/perldoc?Switch#LIMITATIONS

Actuellement, le contenu de l'élément __DATA__ n'est pas filtré.

Actuellement, la longueur des tampons internes est limitée à 32 bits seulement.

LES CHOSES À SURVEILLER

Certains filtres englobent les DATA Traiter

Certains filtres de source utilisent la poignée DATA handle pour lire le programme appelant. Lorsque vous utilisez ces filtres source, vous ne pouvez pas vous fier à ce handle, ni vous attendre à un type de comportement particulier en opérant sur lui. Les filtres basés sur Filter::Util::Call (et donc Filter::Simple) ne modifient pas le handle DATA mais, d'autre part, ignorent totalement le texte après __DATA__.

EXIGENCES

La distribution de Source Filters est disponible sur CPAN, dans la section

CPAN/modules/by-module/Filter

À partir de Perl 5.8 Filter::Util::Call (la partie centrale de la distribution Source Filters) fait partie de la distribution standard de Perl. Est également incluse une interface plus conviviale appelée Filter::Simple, par Damian Conway.

AUTEUR

Paul Marquess <[email protected]>

Reini Urban <[email protected]>

Droits d'auteur

La première version de cet article est apparue à l'origine dans The Perl Journal #11, et est copyright 1998 The Perl Journal. Il apparaît avec l'aimable autorisation de Jon Orwant et de The Perl Journal. Ce document peut être distribué selon les mêmes termes que Perl lui-même.