diff --git a/Conteneur_Fichier.py b/Conteneur_Fichier.py index 3a2319c..fe644fe 100644 --- a/Conteneur_Fichier.py +++ b/Conteneur_Fichier.py @@ -1,214 +1,214 @@ -#!/usr/bin/python -# -*- coding: iso-8859-1 -*- -""" -Conteneur_Fichier - ExeFilter - -Module qui contient la classe L{Conteneur_Fichier.Conteneur_Fichier}, -pour traiter un conteneur qui correspond à un simple fichier. - -Ce fichier fait partie du projet ExeFilter. -URL du projet: U{http://www.decalage.info/exefilter} - -@organization: DGA/CELAR -@author: U{Philippe Lagadec} -@author: U{Arnaud Kerréneur} - -@contact: U{Philippe Lagadec} - -@copyright: DGA/CELAR 2004-2008 -@copyright: NATO/NC3A 2008-2010 (modifications PL apres ExeFilter v1.1.0) - -@license: CeCILL (open-source compatible GPL) - cf. code source ou fichier LICENCE.txt joint - -@version: 1.08 - -@status: beta -""" -#============================================================================== -__docformat__ = 'epytext en' - -__date__ = "2011-04-17" -__version__ = "1.08" - -#------------------------------------------------------------------------------ -# LICENCE pour le projet ExeFilter: - -# Copyright DGA/CELAR 2004-2008 -# Copyright NATO/NC3A 2008-2010 (PL changes after ExeFilter v1.1.0) -# Auteurs: -# - Philippe Lagadec (PL) - philippe.lagadec(a)laposte.net -# - Arnaud Kerréneur (AK) - arnaud.kerreneur(a)dga.defense.gouv.fr -# - Tanguy Vinceleux (TV) - tanguy.vinceleux(a)dga.defense.gouv.fr -# -# Ce logiciel est régi par la licence CeCILL soumise au droit français et -# respectant les principes de diffusion des logiciels libres. Vous pouvez -# utiliser, modifier et/ou redistribuer ce programme sous les conditions -# de la licence CeCILL telle que diffusée par le CEA, le CNRS et l'INRIA -# sur le site "http://www.cecill.info". Une copie de cette licence est jointe -# dans les fichiers Licence_CeCILL_V2-fr.html et Licence_CeCILL_V2-en.html. -# -# En contrepartie de l'accessibilité au code source et des droits de copie, -# de modification et de redistribution accordés par cette licence, il n'est -# offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, -# seule une responsabilité restreinte pèse sur l'auteur du programme, le -# titulaire des droits patrimoniaux et les concédants successifs. -# -# A cet égard l'attention de l'utilisateur est attirée sur les risques -# associés au chargement, à l'utilisation, à la modification et/ou au -# développement et à la reproduction du logiciel par l'utilisateur étant -# donné sa spécificité de logiciel libre, qui peut le rendre complexe à -# manipuler et qui le réserve donc à des développeurs et des professionnels -# avertis possédant des connaissances informatiques approfondies. Les -# utilisateurs sont donc invités à charger et tester l'adéquation du -# logiciel à leurs besoins dans des conditions permettant d'assurer la -# sécurité de leurs systèmes et ou de leurs données et, plus généralement, -# à l'utiliser et l'exploiter dans les mêmes conditions de sécurité. -# -# Le fait que vous puissiez accéder à cet en-tête signifie que vous avez -# pris connaissance de la licence CeCILL, et que vous en avez accepté les -# termes. - -#------------------------------------------------------------------------------ -# HISTORIQUE: -# 18/05/2005 v0.01 PL: - 1ère version -# 2005-2007 PL,AK: - évolutions -# - contributions de Y. Bidan et C. Catherin -# 12/01/2007 v1.00 PL: - version 1.00 officielle -# 2007-11-03 v1.01 PL: - ajout licence CeCILL -# 2008-03-24 v1.02 PL: - ajout de _() pour traduction gettext des chaines -# 2008-04-20 v1.03 PL: - ajout parametre politique a Conteneur_Fichier.__init__ -# 2009-11-11 v1.04 PL: - added option to have a filename as destination -# 2010-02-04 v1.05 PL: - fixed temp dir deletion -# 2010-02-07 v1.06 PL: - removed path module import -# 2010-04-20 v1.07 PL: - added force_extension attrib to Conteneur_Fichier -# 2011-04-17 v1.08 PL: - code to delete temp dir moved to Conteneur - -#------------------------------------------------------------------------------ -# A FAIRE: -#------------------------------------------------------------------------------ - -#=== IMPORTS ================================================================== - -import os - -# modules du projet: -from commun import * -import Conteneur -import Conteneur_Repertoire -import Fichier - -#=== CONSTANTES =============================================================== - - -#=== CLASSES ================================================================== -#------------------------------------------------------------------------------ -# class CONTENEUR_FICHIER -#---------------------------- -class Conteneur_Fichier (Conteneur_Repertoire.Conteneur_Repertoire): - """ - classe pour analyser un fichier simple (c'est en fait un - L{Conteneur_Repertoire.Conteneur_Repertoire} qui ne contient qu'un fichier). - - Un objet Conteneur correspond à un répertoire ou à un fichier - Contient un ensemble de fichiers, par exemple une archive Zip. - La classe Conteneur_Fichier correspond à un simple fichier. - (nécessaire quand on ne veut analyser qu'un fichier) - """ - - def __init__(self, nom_fichier, repertoire_destination, rep_relatif_source, - fichier=None, politique=None, dest_is_a_file=False, force_extension=None): - """ - Constructeur d'objet Conteneur_Fichier. - - @param nom_fichier: le nom du fichier - @type nom_fichier: str - - @param repertoire_destination: le répertoire destination où copier le - fichier apres transfert - @type repertoire_destination: str - - @param dest_is_a_file: False if repertoire_destination is a dir (default), - True if it's a filename. - @type dest_is_a_file: bool - - @param force_extension: if set, force filename extension to a specific value - (used to control which filters are applied) - Note: force_extension may be "" or must start with a dot - @type force_extension: str, unicode - """ - # nom source est le chemin absolu du répertoire source: - chem_source = path(nom_fichier).abspath().normpath().dirname() - Journal.debug(u"chem_source = %s" % chem_source) - self.dest_is_a_file = dest_is_a_file - self.force_extension = force_extension - if dest_is_a_file: - # if dest is a file, store filename and use its parent dir: - self.dest_filename = os.path.abspath(repertoire_destination) - repertoire_destination = os.path.dirname(self.dest_filename) - # on appelle d'abord le constructeur de base - #self.type = "Fichier" - Conteneur.Conteneur.__init__(self, chem_source, repertoire_destination, - rep_relatif_source, fichier, politique) - self.type = _(u"Fichier") - # on sauve le nom de fichier: - self.nom_fichier = path(nom_fichier).name - print self - - - def lister_fichiers (self): - """ - retourne la liste contenant l'objet Fichier, qui n'est - lue qu'une fois au 1er appel. - """ - # pour le chemin du fichier on ne garde que le - # chemin relatif par rapport au répertoire - if len(self.liste_fichiers) == 0: - f = Fichier.Fichier(self.nom_fichier, conteneur=self, - force_extension=self.force_extension) - self.liste_fichiers.append(f) - return self.liste_fichiers - - - def reconstruire (self): - """ - reconstruit le Conteneur à partir des fichiers nettoyés. - """ - # if destination is a directory, use the normal method: - if not self.dest_is_a_file: - Conteneur_Repertoire.Conteneur_Repertoire.reconstruire(self) - # else destination is a file: - else: - # check if there's only one file: - assert(len(self.liste_fichiers) == 1) - fichier = self.liste_fichiers[0] - # si le fichier n'est pas refuse, on le recopie a destination: - if not fichier.resultat_fichier.est_refuse(): - # directory of dest file: - chem_dest_fich = path(os.path.dirname(self.dest_filename)) -## Journal.debug(u"self.chem_dest = %s" % self.chem_dest) -## Journal.debug(u"fichier.chemin.parent = %s" % fichier.chemin.parent) -## Journal.debug(u"fichier.chemin = %s" % fichier.chemin) -## Journal.debug(u"chem_dest_fich = %s" % chem_dest_fich) - # create dest dir if it does not exist: - if not chem_dest_fich.exists(): - chem_dest_fich.makedirs() - fichier_dest = self.dest_filename - Journal.info2(_(u'Copie vers la destination: "%s" -> "%s"...') % (fichier._copie_temp, fichier_dest)) - fichier._copie_temp.copy2(fichier_dest) - # no archiving in that mode: -## # Si l'option archivage (archive_after) est activee: -## if self.politique.parametres['archive_after'].valeur: -## # on copie les fichiers nettoyés vers le répertoire archivage -## #TODO: code a simplifier -## chem_dest_fich = path(self.rep_archive) / path(commun.sous_rep_archive) / self.rep_relatif_source / fichier.chemin.parent -## if not chem_dest_fich.exists(): -## chem_dest_fich.makedirs() -## fichier_dest = path(self.rep_archive) / path(commun.sous_rep_archive) / self.rep_relatif_source / fichier.chemin -## debug(_(u'Copie: "%s" -> "%s"...') % (fichier._copie_temp, fichier_dest)) -## fichier._copie_temp.copy2(fichier_dest) - -## # puis détruire le répertoire temporaire ! -## debug ("Effacement du repertoire temporaire %s" % self.rep_temp_complet) -## self.rep_temp_complet.rmtree() - +#!/usr/bin/python +# -*- coding: iso-8859-1 -*- +""" +Conteneur_Fichier - ExeFilter + +Module qui contient la classe L{Conteneur_Fichier.Conteneur_Fichier}, +pour traiter un conteneur qui correspond à un simple fichier. + +Ce fichier fait partie du projet ExeFilter. +URL du projet: U{http://www.decalage.info/exefilter} + +@organization: DGA/CELAR +@author: U{Philippe Lagadec} +@author: U{Arnaud Kerréneur} + +@contact: U{Philippe Lagadec} + +@copyright: DGA/CELAR 2004-2008 +@copyright: NATO/NC3A 2008-2010 (modifications PL apres ExeFilter v1.1.0) + +@license: CeCILL (open-source compatible GPL) + cf. code source ou fichier LICENCE.txt joint + +@version: 1.08 + +@status: beta +""" +#============================================================================== +__docformat__ = 'epytext en' + +__date__ = "2011-04-17" +__version__ = "1.08" + +#------------------------------------------------------------------------------ +# LICENCE pour le projet ExeFilter: + +# Copyright DGA/CELAR 2004-2008 +# Copyright NATO/NC3A 2008-2010 (PL changes after ExeFilter v1.1.0) +# Auteurs: +# - Philippe Lagadec (PL) - philippe.lagadec(a)laposte.net +# - Arnaud Kerréneur (AK) - arnaud.kerreneur(a)dga.defense.gouv.fr +# - Tanguy Vinceleux (TV) - tanguy.vinceleux(a)dga.defense.gouv.fr +# +# Ce logiciel est régi par la licence CeCILL soumise au droit français et +# respectant les principes de diffusion des logiciels libres. Vous pouvez +# utiliser, modifier et/ou redistribuer ce programme sous les conditions +# de la licence CeCILL telle que diffusée par le CEA, le CNRS et l'INRIA +# sur le site "http://www.cecill.info". Une copie de cette licence est jointe +# dans les fichiers Licence_CeCILL_V2-fr.html et Licence_CeCILL_V2-en.html. +# +# En contrepartie de l'accessibilité au code source et des droits de copie, +# de modification et de redistribution accordés par cette licence, il n'est +# offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, +# seule une responsabilité restreinte pèse sur l'auteur du programme, le +# titulaire des droits patrimoniaux et les concédants successifs. +# +# A cet égard l'attention de l'utilisateur est attirée sur les risques +# associés au chargement, à l'utilisation, à la modification et/ou au +# développement et à la reproduction du logiciel par l'utilisateur étant +# donné sa spécificité de logiciel libre, qui peut le rendre complexe à +# manipuler et qui le réserve donc à des développeurs et des professionnels +# avertis possédant des connaissances informatiques approfondies. Les +# utilisateurs sont donc invités à charger et tester l'adéquation du +# logiciel à leurs besoins dans des conditions permettant d'assurer la +# sécurité de leurs systèmes et ou de leurs données et, plus généralement, +# à l'utiliser et l'exploiter dans les mêmes conditions de sécurité. +# +# Le fait que vous puissiez accéder à cet en-tête signifie que vous avez +# pris connaissance de la licence CeCILL, et que vous en avez accepté les +# termes. + +#------------------------------------------------------------------------------ +# HISTORIQUE: +# 18/05/2005 v0.01 PL: - 1ère version +# 2005-2007 PL,AK: - évolutions +# - contributions de Y. Bidan et C. Catherin +# 12/01/2007 v1.00 PL: - version 1.00 officielle +# 2007-11-03 v1.01 PL: - ajout licence CeCILL +# 2008-03-24 v1.02 PL: - ajout de _() pour traduction gettext des chaines +# 2008-04-20 v1.03 PL: - ajout parametre politique a Conteneur_Fichier.__init__ +# 2009-11-11 v1.04 PL: - added option to have a filename as destination +# 2010-02-04 v1.05 PL: - fixed temp dir deletion +# 2010-02-07 v1.06 PL: - removed path module import +# 2010-04-20 v1.07 PL: - added force_extension attrib to Conteneur_Fichier +# 2011-04-17 v1.08 PL: - code to delete temp dir moved to Conteneur + +#------------------------------------------------------------------------------ +# A FAIRE: +#------------------------------------------------------------------------------ + +#=== IMPORTS ================================================================== + +import os + +# modules du projet: +from commun import * +import Conteneur +import Conteneur_Repertoire +import Fichier + +#=== CONSTANTES =============================================================== + + +#=== CLASSES ================================================================== +#------------------------------------------------------------------------------ +# class CONTENEUR_FICHIER +#---------------------------- +class Conteneur_Fichier (Conteneur_Repertoire.Conteneur_Repertoire): + """ + classe pour analyser un fichier simple (c'est en fait un + L{Conteneur_Repertoire.Conteneur_Repertoire} qui ne contient qu'un fichier). + + Un objet Conteneur correspond à un répertoire ou à un fichier + Contient un ensemble de fichiers, par exemple une archive Zip. + La classe Conteneur_Fichier correspond à un simple fichier. + (nécessaire quand on ne veut analyser qu'un fichier) + """ + + def __init__(self, nom_fichier, repertoire_destination, rep_relatif_source, + fichier=None, politique=None, dest_is_a_file=False, force_extension=None): + """ + Constructeur d'objet Conteneur_Fichier. + + @param nom_fichier: le nom du fichier + @type nom_fichier: str + + @param repertoire_destination: le répertoire destination où copier le + fichier apres transfert + @type repertoire_destination: str + + @param dest_is_a_file: False if repertoire_destination is a dir (default), + True if it's a filename. + @type dest_is_a_file: bool + + @param force_extension: if set, force filename extension to a specific value + (used to control which filters are applied) + Note: force_extension may be "" or must start with a dot + @type force_extension: str, unicode + """ + # nom source est le chemin absolu du répertoire source: + chem_source = path(nom_fichier).abspath().normpath().dirname() + Journal.debug(u"chem_source = %s" % chem_source) + self.dest_is_a_file = dest_is_a_file + self.force_extension = force_extension + if dest_is_a_file: + # if dest is a file, store filename and use its parent dir: + self.dest_filename = os.path.abspath(repertoire_destination) + repertoire_destination = os.path.dirname(self.dest_filename) + # on appelle d'abord le constructeur de base + #self.type = "Fichier" + Conteneur.Conteneur.__init__(self, chem_source, repertoire_destination, + rep_relatif_source, fichier, politique) + self.type = _(u"Fichier") + # on sauve le nom de fichier: + self.nom_fichier = path(nom_fichier).name + print(self) + + + def lister_fichiers (self): + """ + retourne la liste contenant l'objet Fichier, qui n'est + lue qu'une fois au 1er appel. + """ + # pour le chemin du fichier on ne garde que le + # chemin relatif par rapport au répertoire + if len(self.liste_fichiers) == 0: + f = Fichier.Fichier(self.nom_fichier, conteneur=self, + force_extension=self.force_extension) + self.liste_fichiers.append(f) + return self.liste_fichiers + + + def reconstruire (self): + """ + reconstruit le Conteneur à partir des fichiers nettoyés. + """ + # if destination is a directory, use the normal method: + if not self.dest_is_a_file: + Conteneur_Repertoire.Conteneur_Repertoire.reconstruire(self) + # else destination is a file: + else: + # check if there's only one file: + assert(len(self.liste_fichiers) == 1) + fichier = self.liste_fichiers[0] + # si le fichier n'est pas refuse, on le recopie a destination: + if not fichier.resultat_fichier.est_refuse(): + # directory of dest file: + chem_dest_fich = path(os.path.dirname(self.dest_filename)) +## Journal.debug(u"self.chem_dest = %s" % self.chem_dest) +## Journal.debug(u"fichier.chemin.parent = %s" % fichier.chemin.parent) +## Journal.debug(u"fichier.chemin = %s" % fichier.chemin) +## Journal.debug(u"chem_dest_fich = %s" % chem_dest_fich) + # create dest dir if it does not exist: + if not chem_dest_fich.exists(): + chem_dest_fich.makedirs() + fichier_dest = self.dest_filename + Journal.info2(_(u'Copie vers la destination: "%s" -> "%s"...') % (fichier._copie_temp, fichier_dest)) + fichier._copie_temp.copy2(fichier_dest) + # no archiving in that mode: +## # Si l'option archivage (archive_after) est activee: +## if self.politique.parametres['archive_after'].valeur: +## # on copie les fichiers nettoyés vers le répertoire archivage +## #TODO: code a simplifier +## chem_dest_fich = path(self.rep_archive) / path(commun.sous_rep_archive) / self.rep_relatif_source / fichier.chemin.parent +## if not chem_dest_fich.exists(): +## chem_dest_fich.makedirs() +## fichier_dest = path(self.rep_archive) / path(commun.sous_rep_archive) / self.rep_relatif_source / fichier.chemin +## debug(_(u'Copie: "%s" -> "%s"...') % (fichier._copie_temp, fichier_dest)) +## fichier._copie_temp.copy2(fichier_dest) + +## # puis détruire le répertoire temporaire ! +## debug ("Effacement du repertoire temporaire %s" % self.rep_temp_complet) +## self.rep_temp_complet.rmtree() + diff --git a/Conteneur_OpenXML.py b/Conteneur_OpenXML.py index 50b727f..ac62b2e 100644 --- a/Conteneur_OpenXML.py +++ b/Conteneur_OpenXML.py @@ -1,189 +1,189 @@ -#!/usr/bin/python -# -*- coding: iso-8859-1 -*- -""" -Conteneur_OpenXML - ExeFilter - -to scan and clean Open XML files (Microsoft Office 2007 and later). - -This file is part of the ExeFilter project. -Project URL: U{http://www.decalage.info/exefilter} - -@author: U{Philippe Lagadec} - -@contact: U{Philippe Lagadec} - -@copyright: Philippe Lagadec 2011 - -@license: CeCILL (open-source compatible GPL) - cf. code source ou fichier LICENCE.txt joint - -@version: 0.01 - -@status: alpha -""" -#============================================================================== -__docformat__ = 'epytext en' - -__date__ = "2011-02-21" -__version__ = "0.01" - -#------------------------------------------------------------------------------ -# LICENCE pour le projet ExeFilter: - -# Copyright DGA/CELAR 2004-2008 -# Copyright NATO/NC3A 2008-2010 (PL changes after ExeFilter v1.1.0) -# Auteurs: -# - Philippe Lagadec (PL) - philippe.lagadec(a)laposte.net -# - Arnaud Kerréneur (AK) - arnaud.kerreneur(a)dga.defense.gouv.fr -# - Tanguy Vinceleux (TV) - tanguy.vinceleux(a)dga.defense.gouv.fr -# -# Ce logiciel est régi par la licence CeCILL soumise au droit français et -# respectant les principes de diffusion des logiciels libres. Vous pouvez -# utiliser, modifier et/ou redistribuer ce programme sous les conditions -# de la licence CeCILL telle que diffusée par le CEA, le CNRS et l'INRIA -# sur le site "http://www.cecill.info". Une copie de cette licence est jointe -# dans les fichiers Licence_CeCILL_V2-fr.html et Licence_CeCILL_V2-en.html. -# -# En contrepartie de l'accessibilité au code source et des droits de copie, -# de modification et de redistribution accordés par cette licence, il n'est -# offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, -# seule une responsabilité restreinte pèse sur l'auteur du programme, le -# titulaire des droits patrimoniaux et les concédants successifs. -# -# A cet égard l'attention de l'utilisateur est attirée sur les risques -# associés au chargement, à l'utilisation, à la modification et/ou au -# développement et à la reproduction du logiciel par l'utilisateur étant -# donné sa spécificité de logiciel libre, qui peut le rendre complexe à -# manipuler et qui le réserve donc à des développeurs et des professionnels -# avertis possédant des connaissances informatiques approfondies. Les -# utilisateurs sont donc invités à charger et tester l'adéquation du -# logiciel à leurs besoins dans des conditions permettant d'assurer la -# sécurité de leurs systèmes et ou de leurs données et, plus généralement, -# à l'utiliser et l'exploiter dans les mêmes conditions de sécurité. -# -# Le fait que vous puissiez accéder à cet en-tête signifie que vous avez -# pris connaissance de la licence CeCILL, et que vous en avez accepté les -# termes. - -#------------------------------------------------------------------------------ -# CHANGELOG: -# 2011-02-21 v0.01 PL: - 1st version - -#------------------------------------------------------------------------------ -# TODO: - -#=== IMPORTS ================================================================== - -# modules standards Python: - -# modules du projet: -from commun import * -import Conteneur_Zip - - -#=== CONSTANTES =============================================================== - - - -#=== CLASSES ================================================================== - -class Conteneur_OpenXML (Conteneur_Zip.Conteneur_Zip): - """ - class to scan and clean Open XML documents (Microsoft Office 2007 and later) - which are in fact Zip archives. - - A Conteneur object corresponds to a directory or a file which contains a set - of files, for example a Zip archive. - Conteneur_OpenXML is based on Conteneur_Zip. - """ - - def __init__(self, nom_archive, nom_destination, fichier, politique): - """ - Constructeur d'objet Conteneur_OpenXML. - - @warning: Si le format de zip n'est pas supporté, peut lever une exception - zipfile.BadZipFile ou zipfile.error. - - @param nom_archive: nom de fichier/répertoire du fichier zip source. - (chemin relatif par rapport au conteneur) - @type nom_archive: str, unicode - - @param nom_destination: nom de fichier/répertoire du conteneur nettoyé. - @type nom_destination: str, unicode - - @param fichier: objet Fichier du conteneur, ou bien None si c'est le - 1er répertoire. - @type fichier: objet L{Fichier.Fichier} ou None - """ - # on appelle d'abord le constructeur de base - Conteneur_Zip.Conteneur_Zip.__init__(self, nom_archive, nom_destination, - fichier, politique) - self.type = _(u"Open XML document") - - - -## def reconstruire (self): -## """ -## reconstruit le Conteneur à partir des fichiers nettoyés. -## """ -## # on commence par établir la liste des fichiers acceptés: -## liste_fichiers_ok = [] -## for fichier in self.liste_fichiers: -## if not fichier.resultat_fichier.est_refuse(): -## liste_fichiers_ok.append(fichier) -## # on ne reconstruit le fichier Zip que s'il y a des fichiers acceptés -## # (sinon un ZipFile vide provoque une exception bizarre...) -## if len(liste_fichiers_ok) != 0: -## Journal.info2(_(u"Recompression du fichier Zip après nettoyage des fichiers...")) -## # on reconstruit d'abord l'archive Zip dans un fichier temporaire, -## # puisqu'il faudra remplacer le fichier déjà obtenu par copie_temp. -## # Obtention d'un nom de fichier zip temporaire: -## #f_zip_temp, chem_zip_temp = tempfile.mkstemp(suffix=".zip", dir=Conteneur.RACINE_TEMP) -## #f_zip_temp, chem_zip_temp = tempfile.mkstemp(suffix=".zip", dir=commun.politique.parametres['rep_temp'].valeur) -## f_zip_temp, chem_zip_temp = newTempFile(suffix=".zip") -## Journal.debug (u"Fichier Zip temporaire: %s" % chem_zip_temp) -## # Ouverture en écriture du fichier Zip temporaire: -## # le mode ZIP_DEFLATED est nécessaire sinon les fichiers ne -## # seront pas recompressés, juste stockés (ZIP_STORED par défaut) -## zip_temp = zipfile_PL.ZipFile_PL (chem_zip_temp, 'w', zipfile.ZIP_DEFLATED) -## # On commence par réinjecter les répertoires qui étaient présents au départ: -## for zipinfo_rep in self.repertoires: -## zip_temp.writestr(zipinfo_rep, "") -## for fichier in self.liste_fichiers: -## if not fichier.resultat_fichier.est_refuse(): -## Journal.debug (u'Compression: "%s"...' % fichier.copie_temp()) -## # on ajoute le fichier dans l'archive : -## # le permier argument permet d'ouvrir le fichier à compresser corretement -## # le deuxième argument permet l'affichage du fichier dans l'archive avec le -## # bon encodage de caractères -## # reconstruction de l'archive zip sans le répertoire temp -## f = file(fichier.copie_temp(), 'rb') -## zip_temp.writestr(fichier.zipinfo, f.read()) -## f.close() -## #zip_temp.write(str_lat1(fichier._copie_temp) , str_oem(fichier.chemin) ) -## # On doit fermer le ZipFile mais aussi le fichier temporaire -## zip_temp.close() -## f_zip_temp.close() -## # on modifie la date du nouveau zip pour correspondre à -## # celle d'origine: -## date_zip = os.path.getmtime(self.fichier._copie_temp) -## os.utime(chem_zip_temp, (date_zip, date_zip)) -## # on remplace la copie temporaire du fichier zip d'origine par -## # la version nettoyée: -## # NOTE: sous Windows on est obligé d'effacer d'abord le fichier -## # d'origine, alors que sous Unix il serait simplement écrasé -## self.fichier._copie_temp.remove() -## #path_zip_temp = path(chem_zip_temp) -## #path_zip_temp.rename(str_lat1(self.fichier._copie_temp)) -## os.rename(chem_zip_temp, self.fichier._copie_temp) -## else: -## Journal.info2(_(u"Aucun fichier accepté dans le Zip: suppression.")) -## self.fichier._copie_temp.remove() -## # pour finir on détruit le répertoire temporaire: -## if self.rep_temp_complet.exists(): -## self.rep_temp_complet.rmtree() - - - - +#!/usr/bin/python +# -*- coding: iso-8859-1 -*- +""" +Conteneur_OpenXML - ExeFilter + +to scan and clean Open XML files (Microsoft Office 2007 and later). + +This file is part of the ExeFilter project. +Project URL: U{http://www.decalage.info/exefilter} + +@author: U{Philippe Lagadec} + +@contact: U{Philippe Lagadec} + +@copyright: Philippe Lagadec 2011 + +@license: CeCILL (open-source compatible GPL) + cf. code source ou fichier LICENCE.txt joint + +@version: 0.01 + +@status: alpha +""" +#============================================================================== +__docformat__ = 'epytext en' + +__date__ = "2011-02-21" +__version__ = "0.01" + +#------------------------------------------------------------------------------ +# LICENCE pour le projet ExeFilter: + +# Copyright DGA/CELAR 2004-2008 +# Copyright NATO/NC3A 2008-2010 (PL changes after ExeFilter v1.1.0) +# Auteurs: +# - Philippe Lagadec (PL) - philippe.lagadec(a)laposte.net +# - Arnaud Kerréneur (AK) - arnaud.kerreneur(a)dga.defense.gouv.fr +# - Tanguy Vinceleux (TV) - tanguy.vinceleux(a)dga.defense.gouv.fr +# +# Ce logiciel est régi par la licence CeCILL soumise au droit français et +# respectant les principes de diffusion des logiciels libres. Vous pouvez +# utiliser, modifier et/ou redistribuer ce programme sous les conditions +# de la licence CeCILL telle que diffusée par le CEA, le CNRS et l'INRIA +# sur le site "http://www.cecill.info". Une copie de cette licence est jointe +# dans les fichiers Licence_CeCILL_V2-fr.html et Licence_CeCILL_V2-en.html. +# +# En contrepartie de l'accessibilité au code source et des droits de copie, +# de modification et de redistribution accordés par cette licence, il n'est +# offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, +# seule une responsabilité restreinte pèse sur l'auteur du programme, le +# titulaire des droits patrimoniaux et les concédants successifs. +# +# A cet égard l'attention de l'utilisateur est attirée sur les risques +# associés au chargement, à l'utilisation, à la modification et/ou au +# développement et à la reproduction du logiciel par l'utilisateur étant +# donné sa spécificité de logiciel libre, qui peut le rendre complexe à +# manipuler et qui le réserve donc à des développeurs et des professionnels +# avertis possédant des connaissances informatiques approfondies. Les +# utilisateurs sont donc invités à charger et tester l'adéquation du +# logiciel à leurs besoins dans des conditions permettant d'assurer la +# sécurité de leurs systèmes et ou de leurs données et, plus généralement, +# à l'utiliser et l'exploiter dans les mêmes conditions de sécurité. +# +# Le fait que vous puissiez accéder à cet en-tête signifie que vous avez +# pris connaissance de la licence CeCILL, et que vous en avez accepté les +# termes. + +#------------------------------------------------------------------------------ +# CHANGELOG: +# 2011-02-21 v0.01 PL: - 1st version + +#------------------------------------------------------------------------------ +# TODO: + +#=== IMPORTS ================================================================== + +# modules standards Python: + +# modules du projet: +from commun import * +import Conteneur_Zip + + +#=== CONSTANTES =============================================================== + + + +#=== CLASSES ================================================================== + +class Conteneur_OpenXML (Conteneur_Zip.Conteneur_Zip): + """ + class to scan and clean Open XML documents (Microsoft Office 2007 and later) + which are in fact Zip archives. + + A Conteneur object corresponds to a directory or a file which contains a set + of files, for example a Zip archive. + Conteneur_OpenXML is based on Conteneur_Zip. + """ + + def __init__(self, nom_archive, nom_destination, fichier, politique): + """ + Constructeur d'objet Conteneur_OpenXML. + + @warning: Si le format de zip n'est pas supporté, peut lever une exception + zipfile.BadZipFile ou zipfile.error. + + @param nom_archive: nom de fichier/répertoire du fichier zip source. + (chemin relatif par rapport au conteneur) + @type nom_archive: str, unicode + + @param nom_destination: nom de fichier/répertoire du conteneur nettoyé. + @type nom_destination: str, unicode + + @param fichier: objet Fichier du conteneur, ou bien None si c'est le + 1er répertoire. + @type fichier: objet L{Fichier.Fichier} ou None + """ + # on appelle d'abord le constructeur de base + Conteneur_Zip.Conteneur_Zip.__init__(self, nom_archive, nom_destination, + fichier, politique) + self.type = _(u"Open XML document") + + + +## def reconstruire (self): +## """ +## reconstruit le Conteneur à partir des fichiers nettoyés. +## """ +## # on commence par établir la liste des fichiers acceptés: +## liste_fichiers_ok = [] +## for fichier in self.liste_fichiers: +## if not fichier.resultat_fichier.est_refuse(): +## liste_fichiers_ok.append(fichier) +## # on ne reconstruit le fichier Zip que s'il y a des fichiers acceptés +## # (sinon un ZipFile vide provoque une exception bizarre...) +## if len(liste_fichiers_ok) != 0: +## Journal.info2(_(u"Recompression du fichier Zip après nettoyage des fichiers...")) +## # on reconstruit d'abord l'archive Zip dans un fichier temporaire, +## # puisqu'il faudra remplacer le fichier déjà obtenu par copie_temp. +## # Obtention d'un nom de fichier zip temporaire: +## #f_zip_temp, chem_zip_temp = tempfile.mkstemp(suffix=".zip", dir=Conteneur.RACINE_TEMP) +## #f_zip_temp, chem_zip_temp = tempfile.mkstemp(suffix=".zip", dir=commun.politique.parametres['rep_temp'].valeur) +## f_zip_temp, chem_zip_temp = newTempFile(suffix=".zip") +## Journal.debug (u"Fichier Zip temporaire: %s" % chem_zip_temp) +## # Ouverture en écriture du fichier Zip temporaire: +## # le mode ZIP_DEFLATED est nécessaire sinon les fichiers ne +## # seront pas recompressés, juste stockés (ZIP_STORED par défaut) +## zip_temp = zipfile_PL.ZipFile_PL (chem_zip_temp, 'w', zipfile.ZIP_DEFLATED) +## # On commence par réinjecter les répertoires qui étaient présents au départ: +## for zipinfo_rep in self.repertoires: +## zip_temp.writestr(zipinfo_rep, "") +## for fichier in self.liste_fichiers: +## if not fichier.resultat_fichier.est_refuse(): +## Journal.debug (u'Compression: "%s"...' % fichier.copie_temp()) +## # on ajoute le fichier dans l'archive : +## # le permier argument permet d'ouvrir le fichier à compresser corretement +## # le deuxième argument permet l'affichage du fichier dans l'archive avec le +## # bon encodage de caractères +## # reconstruction de l'archive zip sans le répertoire temp +## f = open(fichier.copie_temp(), 'rb') +## zip_temp.writestr(fichier.zipinfo, f.read()) +## f.close() +## #zip_temp.write(str_lat1(fichier._copie_temp) , str_oem(fichier.chemin) ) +## # On doit fermer le ZipFile mais aussi le fichier temporaire +## zip_temp.close() +## f_zip_temp.close() +## # on modifie la date du nouveau zip pour correspondre à +## # celle d'origine: +## date_zip = os.path.getmtime(self.fichier._copie_temp) +## os.utime(chem_zip_temp, (date_zip, date_zip)) +## # on remplace la copie temporaire du fichier zip d'origine par +## # la version nettoyée: +## # NOTE: sous Windows on est obligé d'effacer d'abord le fichier +## # d'origine, alors que sous Unix il serait simplement écrasé +## self.fichier._copie_temp.remove() +## #path_zip_temp = path(chem_zip_temp) +## #path_zip_temp.rename(str_lat1(self.fichier._copie_temp)) +## os.rename(chem_zip_temp, self.fichier._copie_temp) +## else: +## Journal.info2(_(u"Aucun fichier accepté dans le Zip: suppression.")) +## self.fichier._copie_temp.remove() +## # pour finir on détruit le répertoire temporaire: +## if self.rep_temp_complet.exists(): +## self.rep_temp_complet.rmtree() + + + + # coded while listening to "TODO" \ No newline at end of file diff --git a/Conteneur_Repertoire.py b/Conteneur_Repertoire.py index 4541d12..d2fc408 100644 --- a/Conteneur_Repertoire.py +++ b/Conteneur_Repertoire.py @@ -1,234 +1,234 @@ -#!/usr/bin/python -# -*- coding: iso-8859-1 -*- -""" -Conteneur_Repertoire - ExeFilter - -Module qui contient la classe L{Conteneur_Repertoire.Conteneur_Repertoire}, -pour traiter les fichiers d'un répertoire. - -Ce fichier fait partie du projet ExeFilter. -URL du projet: U{http://www.decalage.info/exefilter} - -@organization: DGA/CELAR -@author: U{Philippe Lagadec} -@author: U{Arnaud Kerréneur} - -@contact: U{Philippe Lagadec} - -@copyright: DGA/CELAR 2004-2008 -@copyright: NATO/NC3A 2008-2010 (modifications PL apres ExeFilter v1.1.0) - -@license: CeCILL (open-source compatible GPL) - cf. code source ou fichier LICENCE.txt joint - -@version: 1.06 - -@status: beta -""" -#============================================================================== -__docformat__ = 'epytext en' - -__date__ = "2011-04-17" -__version__ = "1.06" - -#------------------------------------------------------------------------------ -# LICENCE pour le projet ExeFilter: - -# Copyright DGA/CELAR 2004-2008 -# Copyright NATO/NC3A 2008-2010 (PL changes after ExeFilter v1.1.0) -# Auteurs: -# - Philippe Lagadec (PL) - philippe.lagadec(a)laposte.net -# - Arnaud Kerréneur (AK) - arnaud.kerreneur(a)dga.defense.gouv.fr -# - Tanguy Vinceleux (TV) - tanguy.vinceleux(a)dga.defense.gouv.fr -# -# Ce logiciel est régi par la licence CeCILL soumise au droit français et -# respectant les principes de diffusion des logiciels libres. Vous pouvez -# utiliser, modifier et/ou redistribuer ce programme sous les conditions -# de la licence CeCILL telle que diffusée par le CEA, le CNRS et l'INRIA -# sur le site "http://www.cecill.info". Une copie de cette licence est jointe -# dans les fichiers Licence_CeCILL_V2-fr.html et Licence_CeCILL_V2-en.html. -# -# En contrepartie de l'accessibilité au code source et des droits de copie, -# de modification et de redistribution accordés par cette licence, il n'est -# offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, -# seule une responsabilité restreinte pèse sur l'auteur du programme, le -# titulaire des droits patrimoniaux et les concédants successifs. -# -# A cet égard l'attention de l'utilisateur est attirée sur les risques -# associés au chargement, à l'utilisation, à la modification et/ou au -# développement et à la reproduction du logiciel par l'utilisateur étant -# donné sa spécificité de logiciel libre, qui peut le rendre complexe à -# manipuler et qui le réserve donc à des développeurs et des professionnels -# avertis possédant des connaissances informatiques approfondies. Les -# utilisateurs sont donc invités à charger et tester l'adéquation du -# logiciel à leurs besoins dans des conditions permettant d'assurer la -# sécurité de leurs systèmes et ou de leurs données et, plus généralement, -# à l'utiliser et l'exploiter dans les mêmes conditions de sécurité. -# -# Le fait que vous puissiez accéder à cet en-tête signifie que vous avez -# pris connaissance de la licence CeCILL, et que vous en avez accepté les -# termes. - -#------------------------------------------------------------------------------ -# HISTORIQUE: -# 24/10/2004 v0.01 PL: - 1ère version -# 2004-2007 PL,AK: - nombreuses évolutions -# - contributions de Y. Bidan et C. Catherin -# 12/01/2007 v1.00 PL: - version 1.00 officielle -# 2007-11-03 v1.01 PL: - ajout licence CeCILL -# 2008-03-24 v1.02 PL: - ajout de _() pour traduction gettext des chaines -# 2008-04-20 v1.03 PL: - ajout parametre politique a Conteneur_Repertoire.__init__ -# - archivage en fonction du parametre 'archive_after' -# 2010-02-04 v1.04 PL: - fixed temp dir deletion -# 2010-02-07 v1.05 PL: - removed path import -# 2011-04-17 v1.06 PL: - code to delete temp dir moved to Conteneur - - -#------------------------------------------------------------------------------ -# A FAIRE: -#------------------------------------------------------------------------------ - -#=== IMPORTS ================================================================== - -# modules standards Python -import os, stat - -# modules du projet: -from commun import * -import commun -import Conteneur -import Fichier -import Resultat - -#=== CONSTANTES =============================================================== - - -#=== CLASSES ================================================================== - -#------------------------------------------------------------------------------ -# class CONTENEUR_REPERTOIRE -#---------------------------- - -class Conteneur_Repertoire (Conteneur.Conteneur): - """ - classe pour analyser un dossier contenant des fichiers. - - un objet Conteneur correspond à un répertoire ou à un fichier - qui contient un ensemble de fichiers, par exemple une archive Zip. - La classe Conteneur_Repertoire correspond à un répertoire. - - """ - - def __init__(self, nom_repertoire, nom_destination, rep_relatif_source, - fichier=None, politique=None): - """ - Constructeur d'objet Conteneur_Repertoire. - - - @param nom_repertoire: nom du répertoire du conteneur source. - (chemin relatif par rapport au conteneur) - @type nom_repertoire: str - - @param nom_destination: nom de fichier/répertoire du conteneur nettoyé. - @type nom_destination: str - - @param fichier: objet Fichier du conteneur. - @type fichier: str - - """ - # nom source est le chemin absolu du répertoire source: - chem_source = path(nom_repertoire).abspath().normpath() - # on appelle d'abord le constructeur de base - Conteneur.Conteneur.__init__(self, chem_source, nom_destination, - rep_relatif_source, fichier, politique) - self.type = _(u"Repertoire") - self.taille_rep = 0 - print self - - - def lister_fichiers (self): - """ - retourne la liste des objets Fichier du répertoire, qui n'est lue qu'une fois au 1er appel. - """ - if len(self.liste_fichiers) == 0: - for fichier in self.chem_src.walkfiles(): - # pour le chemin du fichier on ne garde que le - # chemin relatif par rapport au répertoire - fichier = self.chem_src.relpathto(fichier) - f = Fichier.Fichier(fichier, conteneur=self) - self.liste_fichiers.append(f) - return self.liste_fichiers - - - def compter_taille_rep (self): - """ - Compte la taille totale des fichiers contenus dans le repertoire. - - @return: taille totale en octets - @rtype: int - """ - if len(self.liste_fichiers) == 0: - for fichier in self.chem_src.walkfiles(): - self.taille_rep += os.stat(fichier).st_size - return self.taille_rep - - - def copie_temp (self, fichier): - """ - copie le fichier vers un répertoire temporaire, et retourne - le chemin de la copie. Cette fonction est normalement appelée - par Fichier.copie_temp() uniquement au 1er appel. - """ - # on s'assure d'abord que le répertoire relatif du fichier existe - # (on suppose que fichier.chemin contient un chemin relatif...) - chem_temp = self.rep_temp_complet / fichier.chemin.parent - Journal.debug(u"rep_temp : %s" % self.rep_temp_complet) - Journal.debug (u"fichier.chemin %s" % fichier.chemin) - Journal.debug(u"chem_temp = %s" % chem_temp) - if not chem_temp.exists(): - chem_temp.makedirs() - fichier_temp = self.rep_temp_complet / fichier.chemin - fichier_source = self.chem_src / fichier.chemin - Journal.info2(_(u'Copie temporaire: "%s" -> "%s"...') % (fichier_source, fichier_temp)) - fichier_source.copy2(fichier_temp) - - # droit en écriture sur le répertoire temporaire pour suppression - #TODO: est-ce necessaire et sur ?? - os.chmod( path(fichier_temp).abspath().normpath(), stat.S_IRWXU ) - - return fichier_temp - - - def reconstruire (self): - """ - reconstruit le Conteneur à partir des fichiers nettoyés. - """ - for fichier in self.liste_fichiers: - # si le fichier n'est pas refuse, on le recopie a destination: - if not fichier.resultat_fichier.est_refuse(): - #TODO: code a simplifier - chem_dest_fich = self.chem_dest / self.rep_relatif_source / fichier.chemin.parent - Journal.debug(u"self.chem_dest = %s" % self.chem_dest) - Journal.debug(u"fichier.chemin.parent = %s" % fichier.chemin.parent) - Journal.debug(u"fichier.chemin = %s" % fichier.chemin) - Journal.debug(u"chem_dest_fich = %s" % chem_dest_fich) - if not chem_dest_fich.exists(): - chem_dest_fich.makedirs() - fichier_dest = self.chem_dest / self.rep_relatif_source / fichier.chemin - Journal.info2(_(u'Copie vers la destination: "%s" -> "%s"...') % (fichier._copie_temp, fichier_dest)) - fichier._copie_temp.copy2(fichier_dest) - - # Si l'option archivage (archive_after) est activee: - if self.politique.parametres['archive_after'].valeur: - # on copie les fichiers nettoyés vers le répertoire archivage - #TODO: code a simplifier - chem_dest_fich = path(self.rep_archive) / path(commun.sous_rep_archive) / self.rep_relatif_source / fichier.chemin.parent - if not chem_dest_fich.exists(): - chem_dest_fich.makedirs() - fichier_dest = path(self.rep_archive) / path(commun.sous_rep_archive) / self.rep_relatif_source / fichier.chemin - debug(_(u'Copie: "%s" -> "%s"...') % (fichier._copie_temp, fichier_dest)) - fichier._copie_temp.copy2(fichier_dest) - -## # puis détruire le répertoire temporaire ! -## debug ("Effacement du repertoire temporaire %s" % self.rep_temp_complet) -## self.rep_temp_complet.rmtree() +#!/usr/bin/python +# -*- coding: iso-8859-1 -*- +""" +Conteneur_Repertoire - ExeFilter + +Module qui contient la classe L{Conteneur_Repertoire.Conteneur_Repertoire}, +pour traiter les fichiers d'un répertoire. + +Ce fichier fait partie du projet ExeFilter. +URL du projet: U{http://www.decalage.info/exefilter} + +@organization: DGA/CELAR +@author: U{Philippe Lagadec} +@author: U{Arnaud Kerréneur} + +@contact: U{Philippe Lagadec} + +@copyright: DGA/CELAR 2004-2008 +@copyright: NATO/NC3A 2008-2010 (modifications PL apres ExeFilter v1.1.0) + +@license: CeCILL (open-source compatible GPL) + cf. code source ou fichier LICENCE.txt joint + +@version: 1.06 + +@status: beta +""" +#============================================================================== +__docformat__ = 'epytext en' + +__date__ = "2011-04-17" +__version__ = "1.06" + +#------------------------------------------------------------------------------ +# LICENCE pour le projet ExeFilter: + +# Copyright DGA/CELAR 2004-2008 +# Copyright NATO/NC3A 2008-2010 (PL changes after ExeFilter v1.1.0) +# Auteurs: +# - Philippe Lagadec (PL) - philippe.lagadec(a)laposte.net +# - Arnaud Kerréneur (AK) - arnaud.kerreneur(a)dga.defense.gouv.fr +# - Tanguy Vinceleux (TV) - tanguy.vinceleux(a)dga.defense.gouv.fr +# +# Ce logiciel est régi par la licence CeCILL soumise au droit français et +# respectant les principes de diffusion des logiciels libres. Vous pouvez +# utiliser, modifier et/ou redistribuer ce programme sous les conditions +# de la licence CeCILL telle que diffusée par le CEA, le CNRS et l'INRIA +# sur le site "http://www.cecill.info". Une copie de cette licence est jointe +# dans les fichiers Licence_CeCILL_V2-fr.html et Licence_CeCILL_V2-en.html. +# +# En contrepartie de l'accessibilité au code source et des droits de copie, +# de modification et de redistribution accordés par cette licence, il n'est +# offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, +# seule une responsabilité restreinte pèse sur l'auteur du programme, le +# titulaire des droits patrimoniaux et les concédants successifs. +# +# A cet égard l'attention de l'utilisateur est attirée sur les risques +# associés au chargement, à l'utilisation, à la modification et/ou au +# développement et à la reproduction du logiciel par l'utilisateur étant +# donné sa spécificité de logiciel libre, qui peut le rendre complexe à +# manipuler et qui le réserve donc à des développeurs et des professionnels +# avertis possédant des connaissances informatiques approfondies. Les +# utilisateurs sont donc invités à charger et tester l'adéquation du +# logiciel à leurs besoins dans des conditions permettant d'assurer la +# sécurité de leurs systèmes et ou de leurs données et, plus généralement, +# à l'utiliser et l'exploiter dans les mêmes conditions de sécurité. +# +# Le fait que vous puissiez accéder à cet en-tête signifie que vous avez +# pris connaissance de la licence CeCILL, et que vous en avez accepté les +# termes. + +#------------------------------------------------------------------------------ +# HISTORIQUE: +# 24/10/2004 v0.01 PL: - 1ère version +# 2004-2007 PL,AK: - nombreuses évolutions +# - contributions de Y. Bidan et C. Catherin +# 12/01/2007 v1.00 PL: - version 1.00 officielle +# 2007-11-03 v1.01 PL: - ajout licence CeCILL +# 2008-03-24 v1.02 PL: - ajout de _() pour traduction gettext des chaines +# 2008-04-20 v1.03 PL: - ajout parametre politique a Conteneur_Repertoire.__init__ +# - archivage en fonction du parametre 'archive_after' +# 2010-02-04 v1.04 PL: - fixed temp dir deletion +# 2010-02-07 v1.05 PL: - removed path import +# 2011-04-17 v1.06 PL: - code to delete temp dir moved to Conteneur + + +#------------------------------------------------------------------------------ +# A FAIRE: +#------------------------------------------------------------------------------ + +#=== IMPORTS ================================================================== + +# modules standards Python +import os, stat + +# modules du projet: +from commun import * +import commun +import Conteneur +import Fichier +import Resultat + +#=== CONSTANTES =============================================================== + + +#=== CLASSES ================================================================== + +#------------------------------------------------------------------------------ +# class CONTENEUR_REPERTOIRE +#---------------------------- + +class Conteneur_Repertoire (Conteneur.Conteneur): + """ + classe pour analyser un dossier contenant des fichiers. + + un objet Conteneur correspond à un répertoire ou à un fichier + qui contient un ensemble de fichiers, par exemple une archive Zip. + La classe Conteneur_Repertoire correspond à un répertoire. + + """ + + def __init__(self, nom_repertoire, nom_destination, rep_relatif_source, + fichier=None, politique=None): + """ + Constructeur d'objet Conteneur_Repertoire. + + + @param nom_repertoire: nom du répertoire du conteneur source. + (chemin relatif par rapport au conteneur) + @type nom_repertoire: str + + @param nom_destination: nom de fichier/répertoire du conteneur nettoyé. + @type nom_destination: str + + @param fichier: objet Fichier du conteneur. + @type fichier: str + + """ + # nom source est le chemin absolu du répertoire source: + chem_source = path(nom_repertoire).abspath().normpath() + # on appelle d'abord le constructeur de base + Conteneur.Conteneur.__init__(self, chem_source, nom_destination, + rep_relatif_source, fichier, politique) + self.type = _(u"Repertoire") + self.taille_rep = 0 + print(self) + + + def lister_fichiers (self): + """ + retourne la liste des objets Fichier du répertoire, qui n'est lue qu'une fois au 1er appel. + """ + if len(self.liste_fichiers) == 0: + for fichier in self.chem_src.walkfiles(): + # pour le chemin du fichier on ne garde que le + # chemin relatif par rapport au répertoire + fichier = self.chem_src.relpathto(fichier) + f = Fichier.Fichier(fichier, conteneur=self) + self.liste_fichiers.append(f) + return self.liste_fichiers + + + def compter_taille_rep (self): + """ + Compte la taille totale des fichiers contenus dans le repertoire. + + @return: taille totale en octets + @rtype: int + """ + if len(self.liste_fichiers) == 0: + for fichier in self.chem_src.walkfiles(): + self.taille_rep += os.stat(fichier).st_size + return self.taille_rep + + + def copie_temp (self, fichier): + """ + copie le fichier vers un répertoire temporaire, et retourne + le chemin de la copie. Cette fonction est normalement appelée + par Fichier.copie_temp() uniquement au 1er appel. + """ + # on s'assure d'abord que le répertoire relatif du fichier existe + # (on suppose que fichier.chemin contient un chemin relatif...) + chem_temp = self.rep_temp_complet / fichier.chemin.parent + Journal.debug(u"rep_temp : %s" % self.rep_temp_complet) + Journal.debug (u"fichier.chemin %s" % fichier.chemin) + Journal.debug(u"chem_temp = %s" % chem_temp) + if not chem_temp.exists(): + chem_temp.makedirs() + fichier_temp = self.rep_temp_complet / fichier.chemin + fichier_source = self.chem_src / fichier.chemin + Journal.info2(_(u'Copie temporaire: "%s" -> "%s"...') % (fichier_source, fichier_temp)) + fichier_source.copy2(fichier_temp) + + # droit en écriture sur le répertoire temporaire pour suppression + #TODO: est-ce necessaire et sur ?? + os.chmod( path(fichier_temp).abspath().normpath(), stat.S_IRWXU ) + + return fichier_temp + + + def reconstruire (self): + """ + reconstruit le Conteneur à partir des fichiers nettoyés. + """ + for fichier in self.liste_fichiers: + # si le fichier n'est pas refuse, on le recopie a destination: + if not fichier.resultat_fichier.est_refuse(): + #TODO: code a simplifier + chem_dest_fich = self.chem_dest / self.rep_relatif_source / fichier.chemin.parent + Journal.debug(u"self.chem_dest = %s" % self.chem_dest) + Journal.debug(u"fichier.chemin.parent = %s" % fichier.chemin.parent) + Journal.debug(u"fichier.chemin = %s" % fichier.chemin) + Journal.debug(u"chem_dest_fich = %s" % chem_dest_fich) + if not chem_dest_fich.exists(): + chem_dest_fich.makedirs() + fichier_dest = self.chem_dest / self.rep_relatif_source / fichier.chemin + Journal.info2(_(u'Copie vers la destination: "%s" -> "%s"...') % (fichier._copie_temp, fichier_dest)) + fichier._copie_temp.copy2(fichier_dest) + + # Si l'option archivage (archive_after) est activee: + if self.politique.parametres['archive_after'].valeur: + # on copie les fichiers nettoyés vers le répertoire archivage + #TODO: code a simplifier + chem_dest_fich = path(self.rep_archive) / path(commun.sous_rep_archive) / self.rep_relatif_source / fichier.chemin.parent + if not chem_dest_fich.exists(): + chem_dest_fich.makedirs() + fichier_dest = path(self.rep_archive) / path(commun.sous_rep_archive) / self.rep_relatif_source / fichier.chemin + debug(_(u'Copie: "%s" -> "%s"...') % (fichier._copie_temp, fichier_dest)) + fichier._copie_temp.copy2(fichier_dest) + +## # puis détruire le répertoire temporaire ! +## debug ("Effacement du repertoire temporaire %s" % self.rep_temp_complet) +## self.rep_temp_complet.rmtree() diff --git a/Conteneur_Zip.py b/Conteneur_Zip.py index 3864574..18b91f6 100644 --- a/Conteneur_Zip.py +++ b/Conteneur_Zip.py @@ -1,330 +1,330 @@ -#!/usr/bin/python -# -*- coding: iso-8859-1 -*- -""" -Conteneur_Zip - ExeFilter - -Module qui contient la classe L{Conteneur_Zip.Conteneur_Zip}, -pour traiter les fichiers d'une archive Zip. - -Ce fichier fait partie du projet ExeFilter. -URL du projet: U{http://www.decalage.info/exefilter} - -@organization: DGA/CELAR -@author: U{Philippe Lagadec} -@author: U{Arnaud Kerréneur} - -@contact: U{Philippe Lagadec} - -@copyright: DGA/CELAR 2004-2008 -@copyright: NATO/NC3A 2008-2010 (modifications PL apres ExeFilter v1.1.0) - -@license: CeCILL (open-source compatible GPL) - cf. code source ou fichier LICENCE.txt joint - -@version: 1.07 - -@status: beta -""" -#============================================================================== -__docformat__ = 'epytext en' - -__date__ = "2011-04-17" -__version__ = "1.07" - -#------------------------------------------------------------------------------ -# LICENCE pour le projet ExeFilter: - -# Copyright DGA/CELAR 2004-2008 -# Copyright NATO/NC3A 2008-2010 (PL changes after ExeFilter v1.1.0) -# Auteurs: -# - Philippe Lagadec (PL) - philippe.lagadec(a)laposte.net -# - Arnaud Kerréneur (AK) - arnaud.kerreneur(a)dga.defense.gouv.fr -# - Tanguy Vinceleux (TV) - tanguy.vinceleux(a)dga.defense.gouv.fr -# -# Ce logiciel est régi par la licence CeCILL soumise au droit français et -# respectant les principes de diffusion des logiciels libres. Vous pouvez -# utiliser, modifier et/ou redistribuer ce programme sous les conditions -# de la licence CeCILL telle que diffusée par le CEA, le CNRS et l'INRIA -# sur le site "http://www.cecill.info". Une copie de cette licence est jointe -# dans les fichiers Licence_CeCILL_V2-fr.html et Licence_CeCILL_V2-en.html. -# -# En contrepartie de l'accessibilité au code source et des droits de copie, -# de modification et de redistribution accordés par cette licence, il n'est -# offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, -# seule une responsabilité restreinte pèse sur l'auteur du programme, le -# titulaire des droits patrimoniaux et les concédants successifs. -# -# A cet égard l'attention de l'utilisateur est attirée sur les risques -# associés au chargement, à l'utilisation, à la modification et/ou au -# développement et à la reproduction du logiciel par l'utilisateur étant -# donné sa spécificité de logiciel libre, qui peut le rendre complexe à -# manipuler et qui le réserve donc à des développeurs et des professionnels -# avertis possédant des connaissances informatiques approfondies. Les -# utilisateurs sont donc invités à charger et tester l'adéquation du -# logiciel à leurs besoins dans des conditions permettant d'assurer la -# sécurité de leurs systèmes et ou de leurs données et, plus généralement, -# à l'utiliser et l'exploiter dans les mêmes conditions de sécurité. -# -# Le fait que vous puissiez accéder à cet en-tête signifie que vous avez -# pris connaissance de la licence CeCILL, et que vous en avez accepté les -# termes. - -#------------------------------------------------------------------------------ -# HISTORIQUE: -# 24/10/2004 v0.01 PL: - 1ère version -# 2004-2007 PL,AK: - nombreuses évolutions -# - contributions de Y. Bidan et C. Catherin -# 12/01/2007 v1.00 PL: - version 1.00 officielle -# 2007-11-03 v1.01 PL: - ajout licence CeCILL -# 2008-03-24 v1.02 PL: - ajout de _() pour traduction gettext des chaines -# 2008-04-20 v1.03 PL: - ajout parametre politique a Conteneur_Zip.__init__ -# 2010-02-07 v1.04 PL: - removed import path -# 2010-02-22 v1.05 PL: - fixed import zipfile_PL -# 2011-02-18 v1.06 PL: - fixed temp file creation using new commun functions -# 2011-04-17 v1.07 PL: - code to delete temp dir moved to Conteneur - -#------------------------------------------------------------------------------ -# A FAIRE: -#------------------------------------------------------------------------------ - -#=== IMPORTS ================================================================== - -# modules standards Python: -import zipfile, tempfile, os, os.path , stat - -# modules du projet: -import commun -from commun import * -import Conteneur, Fichier, Resultat -import thirdparty.zipfile_PL.zipfile_PL as zipfile_PL - - -#=== CONSTANTES =============================================================== - -# Attributs MSDOS/Windows pour les fichiers: -# constantes extraites de win32con.py, dans les extensions Python Win32: -ATTRIB_READONLY = 1 -ATTRIB_HIDDEN = 2 -ATTRIB_SYSTEM = 4 -ATTRIB_LABEL = 8 # ajouté, n'était pas dans win32con -ATTRIB_DIRECTORY = 16 -ATTRIB_ARCHIVE = 32 -ATTRIB_64 = 64 # ajouté, n'était pas dans win32con. Attribut inutilisé ? -ATTRIB_NORMAL = 128 # à quoi sert cet attribut ?? - -# permissions Unix, décalées de 16 bits dans les fichiers zip: cf. aide du -# module stat, et code source de zipfile.py... -ATTRIB_DIR_UNIX = stat.S_IFDIR << 16L - - -#=== CLASSES ================================================================== - -#------------------------------------------------------------------------------ -# class CONTENEUR_ZIP -#--------------------- -class Conteneur_Zip (Conteneur.Conteneur): - """Classe pour analyser une archive Zip contenant des fichiers. - - un objet Conteneur correspond à un répertoire ou à un fichier - qui contient un ensemble de fichiers, par exemple une archive Zip. - La classe Conteneur_Zip correspond à une archive Zip. - """ - - def __init__(self, nom_archive, nom_destination, fichier, politique): - """ - Constructeur d'objet Conteneur_Zip. - - @warning: Si le format de zip n'est pas supporté, peut lever une exception - zipfile.BadZipFile ou zipfile.error. - - @param nom_archive: nom de fichier/répertoire du fichier zip source. - (chemin relatif par rapport au conteneur) - @type nom_archive: str, unicode - - @param nom_destination: nom de fichier/répertoire du conteneur nettoyé. - @type nom_destination: str, unicode - - @param fichier: objet Fichier du conteneur, ou bien None si c'est le - 1er répertoire. - @type fichier: objet L{Fichier.Fichier} ou None - """ - # on appelle d'abord le constructeur de base - Conteneur.Conteneur.__init__(self, nom_archive, nom_destination, "", - fichier, politique) - self.type = _(u"Archive Zip") - # on ouvre le fichier Zip: - Journal.info2(_(u"Ouverture du fichier ZIP grâce au module zipfile.")) - self.politique = politique - self.zip = zipfile_PL.ZipFile_PL(nom_archive) - - -# now there is no need for a different method for creer_rep_tem here: -## def creer_rep_temp(self): -## """Pour initialiser le répertoire temporaire nécessaire à l'analyse du -## conteneur.""" -## # rep_temp va différer si le conteneur est une archive Zip ou si c'est un rép -## # répertoire temporaire: un nouveau sous-répertoire dans RACINE_TEMP -## # mkdtemp permet d'avoir un répertoire sécurisé, accessible -## # seulement à l'utilisateur -## #self.rep_temp = path( tempfile.mkdtemp(dir=Conteneur.RACINE_TEMP) ) -## self.rep_temp_complet = path( tempfile.mkdtemp(dir=commun.politique.parametres['rep_temp'].valeur) ) - - - def lister_fichiers (self): - """Retourne la liste des fichiers de l'archive Zip, qui n'est - lue qu'une fois au 1er appel. - - @return: liste des fichiers contenus - @rtype : liste de L{Fichier.Fichier} - """ - # on ne dresse la liste des fichiers que la première fois: - if len(self.liste_fichiers) == 0: - # liste des répertoires contenus (objets ZipInfo) - self.repertoires = [] - Journal.info2(_(u"Lecture de la liste des fichiers du fichier ZIP:")) - for zipinfo in self.zip.infolist(): - # on convertit le nom de fichier en unicode en utilisant le - # codec "cp850", car dans un zip les noms de fichiers sont - # au format OEM (MS-DOS): sinon erreur en cas d'accents - nom_fichier = unicode(zipinfo.filename, 'cp850') - Journal.info2(_(u"- fichier: %s") % nom_fichier) - Journal.info2( - " zipinfo: flag_bits = %d, internal_attr = %d, external_attr = %d" - % (zipinfo.flag_bits, zipinfo.internal_attr, zipinfo.external_attr)) - Journal.info2( - " compress_type = %d, create_system = %d, create_version = %d" - % (zipinfo.compress_type, zipinfo.create_system, zipinfo.create_version)) - Journal.info2( - " extract_version = %d, volume = %d" - % (zipinfo.extract_version, zipinfo.volume)) - Journal.info2( - " compress_size = %d, file_size = %d" - % (zipinfo.compress_size, zipinfo.file_size)) - # les répertoires et labels doivent être traités à part, ils ne - # sont pas dans la liste des fichiers - if zipinfo.external_attr & (ATTRIB_DIRECTORY | ATTRIB_DIR_UNIX) : - # Si c'est un répertoire on ajoute le zipinfo à la liste: - self.repertoires.append(zipinfo) - Journal.info2(_(u" Répertoire.")) - elif zipinfo.external_attr & ATTRIB_LABEL : - # Si c'est un label on l'ignore: - Journal.info2(_(u" Cette entrée est un label de volume MS-DOS, on l'ignore.")) - else: - # C'est un fichier, on l'ajoute à la liste. - # si le nom est vide ou "-", ce n'est pas un fichier mais - # un texte issu de l'entrée standard (cf. appnote.iz) - # il faut aussi vérifier que ce n'est pas un chemin absolu, - # et convertir tous les antislashs en slashs - f = Fichier.Fichier(nom_fichier, conteneur=self) - # on ajoute un nouvel attribut zipinfo au fichier - f.zipinfo = zipinfo - self.liste_fichiers.append(f) - return self.liste_fichiers - - - def copie_temp (self, fichier): - """copie le fichier vers un répertoire temporaire, et retourne - le chemin de la copie. Cette fonction est normalement appelée - par Fichier.copie_temp() uniquement au 1er appel.""" - # on s'assure d'abord que le répertoire relatif du fichier existe - # (on suppose que fichier.chemin contient un chemin relatif...) - chem_temp = self.rep_temp_complet / fichier.chemin.parent - if not chem_temp.exists(): - chem_temp.makedirs() - fichier_temp = self.rep_temp_complet / fichier.chemin - Journal.info2(_(u'Décompression: "%s" -> "%s"...') % (fichier.chemin, fichier_temp)) - - # droit en écriture sur le répertoire temporaire pour suppression -## os.chmod( path(fichier_temp).abspath().normpath(), stat.S_IRWXU ) - - f = file(fichier_temp, 'wb') - # ici on encode le nom de fichier en OEM (cp850) car c'est le format interne zip - f.write(self.zip.read(fichier.chemin.encode('cp850'))) - f.close() - return fichier_temp - - - def fermer (self): - """ - Ferme le conteneur une fois que tous les fichiers inclus ont - été analysés. - """ - self.zip.close() - - - def reconstruire (self): - """ - reconstruit le Conteneur à partir des fichiers nettoyés. - """ - # on commence par établir la liste des fichiers acceptés: - liste_fichiers_ok = [] - for fichier in self.liste_fichiers: - if not fichier.resultat_fichier.est_refuse(): - liste_fichiers_ok.append(fichier) - # on ne reconstruit le fichier Zip que s'il y a des fichiers acceptés - # (sinon un ZipFile vide provoque une exception bizarre...) - if len(liste_fichiers_ok) != 0: - Journal.info2(_(u"Recompression du fichier Zip après nettoyage des fichiers...")) - # on reconstruit d'abord l'archive Zip dans un fichier temporaire, - # puisqu'il faudra remplacer le fichier déjà obtenu par copie_temp. - # Obtention d'un nom de fichier zip temporaire: - #f_zip_temp, chem_zip_temp = tempfile.mkstemp(suffix=".zip", dir=Conteneur.RACINE_TEMP) - #f_zip_temp, chem_zip_temp = tempfile.mkstemp(suffix=".zip", dir=commun.politique.parametres['rep_temp'].valeur) - f_zip_temp, chem_zip_temp = newTempFile(suffix=".zip") - Journal.debug (u"Fichier Zip temporaire: %s" % chem_zip_temp) - # Ouverture en écriture du fichier Zip temporaire: - # le mode ZIP_DEFLATED est nécessaire sinon les fichiers ne - # seront pas recompressés, juste stockés (ZIP_STORED par défaut) - zip_temp = zipfile_PL.ZipFile_PL (chem_zip_temp, 'w', zipfile.ZIP_DEFLATED) - # On commence par réinjecter les répertoires qui étaient présents au départ: - for zipinfo_rep in self.repertoires: - zip_temp.writestr(zipinfo_rep, "") - for fichier in self.liste_fichiers: - if not fichier.resultat_fichier.est_refuse(): - Journal.debug (u'Compression: "%s"...' % fichier.copie_temp()) - # on ajoute le fichier dans l'archive : - # le permier argument permet d'ouvrir le fichier à compresser corretement - # le deuxième argument permet l'affichage du fichier dans l'archive avec le - # bon encodage de caractères - # reconstruction de l'archive zip sans le répertoire temp - f = file(fichier.copie_temp(), 'rb') - zip_temp.writestr(fichier.zipinfo, f.read()) - f.close() - #zip_temp.write(str_lat1(fichier._copie_temp) , str_oem(fichier.chemin) ) - # On doit fermer le ZipFile mais aussi le fichier temporaire - zip_temp.close() - f_zip_temp.close() - # on modifie la date du nouveau zip pour correspondre à - # celle d'origine: - date_zip = os.path.getmtime(self.fichier._copie_temp) - os.utime(chem_zip_temp, (date_zip, date_zip)) - # on remplace la copie temporaire du fichier zip d'origine par - # la version nettoyée: - # NOTE: sous Windows on est obligé d'effacer d'abord le fichier - # d'origine, alors que sous Unix il serait simplement écrasé - self.fichier._copie_temp.remove() - #path_zip_temp = path(chem_zip_temp) - #path_zip_temp.rename(str_lat1(self.fichier._copie_temp)) - os.rename(chem_zip_temp, self.fichier._copie_temp) - else: - Journal.info2(_(u"Aucun fichier accepté dans le Zip: suppression.")) - self.fichier._copie_temp.remove() -## # pour finir on détruit le répertoire temporaire: -## if self.rep_temp_complet.exists(): -## self.rep_temp_complet.rmtree() - - - def est_chiffre(self, fichier): - """Retourne True si le fichier indiqué est chiffré, et qu'il ne peut - pas être extrait du fichier ZIP. - - @param fichier: fichier à tester. - @type fichier: objet L{Fichier} - """ - # le bit 0 du champ "general purpose bit flag" est normalement - # positionné à 1 si le fichier est chiffré, quel que soit l'algo de - # chiffrement. (cf. appnote.iz ou appnote.txt) - if fichier.zipinfo.flag_bits & 1: - return True - else: - return False +#!/usr/bin/python +# -*- coding: iso-8859-1 -*- +""" +Conteneur_Zip - ExeFilter + +Module qui contient la classe L{Conteneur_Zip.Conteneur_Zip}, +pour traiter les fichiers d'une archive Zip. + +Ce fichier fait partie du projet ExeFilter. +URL du projet: U{http://www.decalage.info/exefilter} + +@organization: DGA/CELAR +@author: U{Philippe Lagadec} +@author: U{Arnaud Kerréneur} + +@contact: U{Philippe Lagadec} + +@copyright: DGA/CELAR 2004-2008 +@copyright: NATO/NC3A 2008-2010 (modifications PL apres ExeFilter v1.1.0) + +@license: CeCILL (open-source compatible GPL) + cf. code source ou fichier LICENCE.txt joint + +@version: 1.07 + +@status: beta +""" +#============================================================================== +__docformat__ = 'epytext en' + +__date__ = "2011-04-17" +__version__ = "1.07" + +#------------------------------------------------------------------------------ +# LICENCE pour le projet ExeFilter: + +# Copyright DGA/CELAR 2004-2008 +# Copyright NATO/NC3A 2008-2010 (PL changes after ExeFilter v1.1.0) +# Auteurs: +# - Philippe Lagadec (PL) - philippe.lagadec(a)laposte.net +# - Arnaud Kerréneur (AK) - arnaud.kerreneur(a)dga.defense.gouv.fr +# - Tanguy Vinceleux (TV) - tanguy.vinceleux(a)dga.defense.gouv.fr +# +# Ce logiciel est régi par la licence CeCILL soumise au droit français et +# respectant les principes de diffusion des logiciels libres. Vous pouvez +# utiliser, modifier et/ou redistribuer ce programme sous les conditions +# de la licence CeCILL telle que diffusée par le CEA, le CNRS et l'INRIA +# sur le site "http://www.cecill.info". Une copie de cette licence est jointe +# dans les fichiers Licence_CeCILL_V2-fr.html et Licence_CeCILL_V2-en.html. +# +# En contrepartie de l'accessibilité au code source et des droits de copie, +# de modification et de redistribution accordés par cette licence, il n'est +# offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, +# seule une responsabilité restreinte pèse sur l'auteur du programme, le +# titulaire des droits patrimoniaux et les concédants successifs. +# +# A cet égard l'attention de l'utilisateur est attirée sur les risques +# associés au chargement, à l'utilisation, à la modification et/ou au +# développement et à la reproduction du logiciel par l'utilisateur étant +# donné sa spécificité de logiciel libre, qui peut le rendre complexe à +# manipuler et qui le réserve donc à des développeurs et des professionnels +# avertis possédant des connaissances informatiques approfondies. Les +# utilisateurs sont donc invités à charger et tester l'adéquation du +# logiciel à leurs besoins dans des conditions permettant d'assurer la +# sécurité de leurs systèmes et ou de leurs données et, plus généralement, +# à l'utiliser et l'exploiter dans les mêmes conditions de sécurité. +# +# Le fait que vous puissiez accéder à cet en-tête signifie que vous avez +# pris connaissance de la licence CeCILL, et que vous en avez accepté les +# termes. + +#------------------------------------------------------------------------------ +# HISTORIQUE: +# 24/10/2004 v0.01 PL: - 1ère version +# 2004-2007 PL,AK: - nombreuses évolutions +# - contributions de Y. Bidan et C. Catherin +# 12/01/2007 v1.00 PL: - version 1.00 officielle +# 2007-11-03 v1.01 PL: - ajout licence CeCILL +# 2008-03-24 v1.02 PL: - ajout de _() pour traduction gettext des chaines +# 2008-04-20 v1.03 PL: - ajout parametre politique a Conteneur_Zip.__init__ +# 2010-02-07 v1.04 PL: - removed import path +# 2010-02-22 v1.05 PL: - fixed import zipfile_PL +# 2011-02-18 v1.06 PL: - fixed temp file creation using new commun functions +# 2011-04-17 v1.07 PL: - code to delete temp dir moved to Conteneur + +#------------------------------------------------------------------------------ +# A FAIRE: +#------------------------------------------------------------------------------ + +#=== IMPORTS ================================================================== + +# modules standards Python: +import zipfile, tempfile, os, os.path , stat + +# modules du projet: +import commun +from commun import * +import Conteneur, Fichier, Resultat +import thirdparty.zipfile_PL.zipfile_PL as zipfile_PL + + +#=== CONSTANTES =============================================================== + +# Attributs MSDOS/Windows pour les fichiers: +# constantes extraites de win32con.py, dans les extensions Python Win32: +ATTRIB_READONLY = 1 +ATTRIB_HIDDEN = 2 +ATTRIB_SYSTEM = 4 +ATTRIB_LABEL = 8 # ajouté, n'était pas dans win32con +ATTRIB_DIRECTORY = 16 +ATTRIB_ARCHIVE = 32 +ATTRIB_64 = 64 # ajouté, n'était pas dans win32con. Attribut inutilisé ? +ATTRIB_NORMAL = 128 # à quoi sert cet attribut ?? + +# permissions Unix, décalées de 16 bits dans les fichiers zip: cf. aide du +# module stat, et code source de zipfile.py... +ATTRIB_DIR_UNIX = stat.S_IFDIR << 16 + + +#=== CLASSES ================================================================== + +#------------------------------------------------------------------------------ +# class CONTENEUR_ZIP +#--------------------- +class Conteneur_Zip (Conteneur.Conteneur): + """Classe pour analyser une archive Zip contenant des fichiers. + + un objet Conteneur correspond à un répertoire ou à un fichier + qui contient un ensemble de fichiers, par exemple une archive Zip. + La classe Conteneur_Zip correspond à une archive Zip. + """ + + def __init__(self, nom_archive, nom_destination, fichier, politique): + """ + Constructeur d'objet Conteneur_Zip. + + @warning: Si le format de zip n'est pas supporté, peut lever une exception + zipfile.BadZipFile ou zipfile.error. + + @param nom_archive: nom de fichier/répertoire du fichier zip source. + (chemin relatif par rapport au conteneur) + @type nom_archive: str, unicode + + @param nom_destination: nom de fichier/répertoire du conteneur nettoyé. + @type nom_destination: str, unicode + + @param fichier: objet Fichier du conteneur, ou bien None si c'est le + 1er répertoire. + @type fichier: objet L{Fichier.Fichier} ou None + """ + # on appelle d'abord le constructeur de base + Conteneur.Conteneur.__init__(self, nom_archive, nom_destination, "", + fichier, politique) + self.type = _(u"Archive Zip") + # on ouvre le fichier Zip: + Journal.info2(_(u"Ouverture du fichier ZIP grâce au module zipfile.")) + self.politique = politique + self.zip = zipfile_PL.ZipFile_PL(nom_archive) + + +# now there is no need for a different method for creer_rep_tem here: +## def creer_rep_temp(self): +## """Pour initialiser le répertoire temporaire nécessaire à l'analyse du +## conteneur.""" +## # rep_temp va différer si le conteneur est une archive Zip ou si c'est un rép +## # répertoire temporaire: un nouveau sous-répertoire dans RACINE_TEMP +## # mkdtemp permet d'avoir un répertoire sécurisé, accessible +## # seulement à l'utilisateur +## #self.rep_temp = path( tempfile.mkdtemp(dir=Conteneur.RACINE_TEMP) ) +## self.rep_temp_complet = path( tempfile.mkdtemp(dir=commun.politique.parametres['rep_temp'].valeur) ) + + + def lister_fichiers (self): + """Retourne la liste des fichiers de l'archive Zip, qui n'est + lue qu'une fois au 1er appel. + + @return: liste des fichiers contenus + @rtype : liste de L{Fichier.Fichier} + """ + # on ne dresse la liste des fichiers que la première fois: + if len(self.liste_fichiers) == 0: + # liste des répertoires contenus (objets ZipInfo) + self.repertoires = [] + Journal.info2(_(u"Lecture de la liste des fichiers du fichier ZIP:")) + for zipinfo in self.zip.infolist(): + # on convertit le nom de fichier en unicode en utilisant le + # codec "cp850", car dans un zip les noms de fichiers sont + # au format OEM (MS-DOS): sinon erreur en cas d'accents + nom_fichier = zipinfo.filename if isinstance(zipinfo.filename, str) else zipinfo.filename.decode('cp850') + Journal.info2(_(u"- fichier: %s") % nom_fichier) + Journal.info2( + " zipinfo: flag_bits = %d, internal_attr = %d, external_attr = %d" + % (zipinfo.flag_bits, zipinfo.internal_attr, zipinfo.external_attr)) + Journal.info2( + " compress_type = %d, create_system = %d, create_version = %d" + % (zipinfo.compress_type, zipinfo.create_system, zipinfo.create_version)) + Journal.info2( + " extract_version = %d, volume = %d" + % (zipinfo.extract_version, zipinfo.volume)) + Journal.info2( + " compress_size = %d, file_size = %d" + % (zipinfo.compress_size, zipinfo.file_size)) + # les répertoires et labels doivent être traités à part, ils ne + # sont pas dans la liste des fichiers + if zipinfo.external_attr & (ATTRIB_DIRECTORY | ATTRIB_DIR_UNIX) : + # Si c'est un répertoire on ajoute le zipinfo à la liste: + self.repertoires.append(zipinfo) + Journal.info2(_(u" Répertoire.")) + elif zipinfo.external_attr & ATTRIB_LABEL : + # Si c'est un label on l'ignore: + Journal.info2(_(u" Cette entrée est un label de volume MS-DOS, on l'ignore.")) + else: + # C'est un fichier, on l'ajoute à la liste. + # si le nom est vide ou "-", ce n'est pas un fichier mais + # un texte issu de l'entrée standard (cf. appnote.iz) + # il faut aussi vérifier que ce n'est pas un chemin absolu, + # et convertir tous les antislashs en slashs + f = Fichier.Fichier(nom_fichier, conteneur=self) + # on ajoute un nouvel attribut zipinfo au fichier + f.zipinfo = zipinfo + self.liste_fichiers.append(f) + return self.liste_fichiers + + + def copie_temp (self, fichier): + """copie le fichier vers un répertoire temporaire, et retourne + le chemin de la copie. Cette fonction est normalement appelée + par Fichier.copie_temp() uniquement au 1er appel.""" + # on s'assure d'abord que le répertoire relatif du fichier existe + # (on suppose que fichier.chemin contient un chemin relatif...) + chem_temp = self.rep_temp_complet / fichier.chemin.parent + if not chem_temp.exists(): + chem_temp.makedirs() + fichier_temp = self.rep_temp_complet / fichier.chemin + Journal.info2(_(u'Décompression: "%s" -> "%s"...') % (fichier.chemin, fichier_temp)) + + # droit en écriture sur le répertoire temporaire pour suppression +## os.chmod( path(fichier_temp).abspath().normpath(), stat.S_IRWXU ) + + f = open(fichier_temp, 'wb') + # ici on encode le nom de fichier en OEM (cp850) car c'est le format interne zip + f.write(self.zip.read(fichier.chemin)) + f.close() + return fichier_temp + + + def fermer (self): + """ + Ferme le conteneur une fois que tous les fichiers inclus ont + été analysés. + """ + self.zip.close() + + + def reconstruire (self): + """ + reconstruit le Conteneur à partir des fichiers nettoyés. + """ + # on commence par établir la liste des fichiers acceptés: + liste_fichiers_ok = [] + for fichier in self.liste_fichiers: + if not fichier.resultat_fichier.est_refuse(): + liste_fichiers_ok.append(fichier) + # on ne reconstruit le fichier Zip que s'il y a des fichiers acceptés + # (sinon un ZipFile vide provoque une exception bizarre...) + if len(liste_fichiers_ok) != 0: + Journal.info2(_(u"Recompression du fichier Zip après nettoyage des fichiers...")) + # on reconstruit d'abord l'archive Zip dans un fichier temporaire, + # puisqu'il faudra remplacer le fichier déjà obtenu par copie_temp. + # Obtention d'un nom de fichier zip temporaire: + #f_zip_temp, chem_zip_temp = tempfile.mkstemp(suffix=".zip", dir=Conteneur.RACINE_TEMP) + #f_zip_temp, chem_zip_temp = tempfile.mkstemp(suffix=".zip", dir=commun.politique.parametres['rep_temp'].valeur) + f_zip_temp, chem_zip_temp = newTempFile(suffix=".zip") + Journal.debug (u"Fichier Zip temporaire: %s" % chem_zip_temp) + # Ouverture en écriture du fichier Zip temporaire: + # le mode ZIP_DEFLATED est nécessaire sinon les fichiers ne + # seront pas recompressés, juste stockés (ZIP_STORED par défaut) + zip_temp = zipfile_PL.ZipFile_PL (chem_zip_temp, 'w', zipfile.ZIP_DEFLATED) + # On commence par réinjecter les répertoires qui étaient présents au départ: + for zipinfo_rep in self.repertoires: + zip_temp.writestr(zipinfo_rep, "") + for fichier in self.liste_fichiers: + if not fichier.resultat_fichier.est_refuse(): + Journal.debug (u'Compression: "%s"...' % fichier.copie_temp()) + # on ajoute le fichier dans l'archive : + # le permier argument permet d'ouvrir le fichier à compresser corretement + # le deuxième argument permet l'affichage du fichier dans l'archive avec le + # bon encodage de caractères + # reconstruction de l'archive zip sans le répertoire temp + f = open(fichier.copie_temp(), 'rb') + zip_temp.writestr(fichier.zipinfo, f.read()) + f.close() + #zip_temp.write(str_lat1(fichier._copie_temp) , str_oem(fichier.chemin) ) + # On doit fermer le ZipFile mais aussi le fichier temporaire + zip_temp.close() + f_zip_temp.close() + # on modifie la date du nouveau zip pour correspondre à + # celle d'origine: + date_zip = os.path.getmtime(self.fichier._copie_temp) + os.utime(chem_zip_temp, (date_zip, date_zip)) + # on remplace la copie temporaire du fichier zip d'origine par + # la version nettoyée: + # NOTE: sous Windows on est obligé d'effacer d'abord le fichier + # d'origine, alors que sous Unix il serait simplement écrasé + self.fichier._copie_temp.remove() + #path_zip_temp = path(chem_zip_temp) + #path_zip_temp.rename(str_lat1(self.fichier._copie_temp)) + os.rename(chem_zip_temp, self.fichier._copie_temp) + else: + Journal.info2(_(u"Aucun fichier accepté dans le Zip: suppression.")) + self.fichier._copie_temp.remove() +## # pour finir on détruit le répertoire temporaire: +## if self.rep_temp_complet.exists(): +## self.rep_temp_complet.rmtree() + + + def est_chiffre(self, fichier): + """Retourne True si le fichier indiqué est chiffré, et qu'il ne peut + pas être extrait du fichier ZIP. + + @param fichier: fichier à tester. + @type fichier: objet L{Fichier} + """ + # le bit 0 du champ "general purpose bit flag" est normalement + # positionné à 1 si le fichier est chiffré, quel que soit l'algo de + # chiffrement. (cf. appnote.iz ou appnote.txt) + if fichier.zipinfo.flag_bits & 1: + return True + else: + return False diff --git a/ExeFilter.py b/ExeFilter.py index 75e798e..4832cf2 100644 --- a/ExeFilter.py +++ b/ExeFilter.py @@ -171,11 +171,12 @@ def init_gettext(): try: gt = gettext.translation("ExeFilter", locale_dir, languages) # puis on installe la fonction de traduction _(): - gt.install(unicode=True) + gt.install() except: # en cas d'erreur, on doit quand meme definir _() comme une fonction # "builtin" qui renvoie la chaine inchangee: - __builtins__._ = lambda text: text + import builtins + builtins._ = lambda text: text else: # si la langue est 'fr', on n'utilise aucune traduction: gettext.NullTranslations().install() @@ -192,17 +193,13 @@ def init_gettext(): import os, sys, time, socket, optparse, tempfile, os.path import threading -# hack to change default encoding to Latin-1 instead of ASCII: -reload(sys) -sys.setdefaultencoding( "latin-1" ) - # modules spécifiques à Windows: if sys.platform == 'win32': try: import win32api , win32security except: - raise ImportError, "the pywin32 module is not installed: "\ - "see http://sourceforge.net/projects/pywin32" + raise ImportError("the pywin32 module is not installed: " + "see http://sourceforge.net/projects/pywin32") # modules d'ExeFilter: from commun import * @@ -493,7 +490,7 @@ def init_archivage(politique, taille_src): if taille_src > politique.parametres['taille_archives'].valeur: msg = _(u"La taille des fichiers source est superieure a la taille du repertoire d'archivage.") Journal.error(msg) - raise RuntimeError, msg + raise RuntimeError(msg) # boucle pour effacer les sous-rép les plus anciens dans le cas où il n'y a pas assez # d'espace disque dans le rép archivage pour copier les fichiers source @@ -523,7 +520,7 @@ def init_archivage(politique, taille_src): # s'il n'y a plus de sous-rép archive à effacer, on génère une exception msg = _(u"repertoire d'archivage deja vide => taille source trop grande") Journal.error(msg) - raise RuntimeError, msg + raise RuntimeError(msg) @@ -621,7 +618,7 @@ def _transfert_not_threadsafe(liste_source, destination, type_transfert="entree" if pol != None: if isinstance(pol, Politique.Politique): p = pol - elif isinstance (pol, [file, str, unicode, list]): + elif isinstance(pol, (str, bytes, list)): p = Politique.Politique(pol) # obsolete, to be removed? elif type_transfert in ("entree", "sortie"): @@ -639,7 +636,7 @@ def _transfert_not_threadsafe(liste_source, destination, type_transfert="entree" # nom des fichiers log = nom de la machine + date et heure du transfert if logfile == 'auto': - print 'logfile=auto' + print('logfile=auto') # generate log filename automatically (one per session): nom_journal_secu = nom_commun + ".log" # set full path in logs folder: @@ -649,7 +646,7 @@ def _transfert_not_threadsafe(liste_source, destination, type_transfert="entree" logs_folder.makedirs() path_logfile = (logs_folder / nom_journal_secu).abspath() elif logfile: - print 'logfile=%s' % logfile + print('logfile=%s' % logfile) # use provided log filename: nom_journal_secu = logfile # set full path directly: @@ -743,7 +740,7 @@ def _transfert_not_threadsafe(liste_source, destination, type_transfert="entree" if taille_src > p.parametres['taille_temp'].valeur: msg = "La taille des fichiers source est superieure a la taille du repertoire temporaire." Journal.error(msg) - raise RuntimeError, msg + raise RuntimeError(msg) # initialisation de l'archivage: if parametres['archive_after'].valeur: @@ -797,7 +794,7 @@ def _transfert_not_threadsafe(liste_source, destination, type_transfert="entree" elif (blocked>0) and (clean+cleaned == 0): exitcode = p.parametres['exitcode_blocked'].valeur else: - raise ValueError, 'Summary values look wrong...' + raise ValueError('Summary values look wrong...') Journal.debug('Exit code: %d' % exitcode) Journal.fermer_journal() @@ -919,12 +916,12 @@ def scan_string(data, filename=None, content_type=None, policy=None, logfile=Non elif content_type is not None: if content_type not in CT_to_ext: #TODO: here we should return a "blocked" result rather than an exception - raise ValueError, 'Content-type not allowed: "%s"' % content_type + raise ValueError('Content-type not allowed: "%s"' % content_type) ext = CT_to_ext[content_type] # use a temporary filename = temp.ext fname = 'temp'+ext else: - raise RuntimeError, 'Either filename or content-type is required' + raise RuntimeError('Either filename or content-type is required') # create a new temporary dir to store the file with its original filename tempdir = tempfilemgr.newTempDir() fpath = os.path.join(tempdir, fname) @@ -963,12 +960,12 @@ def clean_string(data, filename=None, content_type=None, policy=None, logfile=No elif content_type is not None: if content_type not in CT_to_ext: #TODO: here we should return a "blocked" result rather than an exception - raise ValueError, 'Content-type not allowed: "%s"' % content_type + raise ValueError('Content-type not allowed: "%s"' % content_type) ext = CT_to_ext[content_type] # use a temporary filename = temp.ext fname = 'temp'+ext else: - raise RuntimeError, 'Either filename or content-type is required' + raise RuntimeError('Either filename or content-type is required') # create a new temporary dir to store the file with its original filename tempdir = tempfilemgr.newTempDir() fpath = os.path.join(tempdir, fname) @@ -999,14 +996,11 @@ def clean_string(data, filename=None, content_type=None, policy=None, logfile=No if __name__ == '__main__': # si compilation py2exe, il faut fixer ici le codec par défaut, car il n'y # a pas de sitecustomize.py: - if hasattr(sys,"setdefaultencoding"): - sys.setdefaultencoding("iso-8859-1") - # Banniere - print "-"*79 - print "ExeFilter v%s - %s" % (XF_VERSION, XF_DATE) - print "-"*79 - print "" + print("-"*79) + print("ExeFilter v%s - %s" % (XF_VERSION, XF_DATE)) + print("-"*79) + print("") # on crée un objet optparse.OptionParser pour analyser la ligne de commande: op = optparse.OptionParser(usage = @@ -1043,8 +1037,8 @@ def clean_string(data, filename=None, content_type=None, policy=None, logfile=No and options.export_html==''\ and options.nouv_politique=='': op.print_help() - print "" - print 'Please specify files to be analyzed, and an optional destination.' + print("") + print('Please specify files to be analyzed, and an optional destination.') ## print _("Il faut indiquer les fichiers/repertoires a nettoyer, ainsi qu'une destination.") sys.exit(parametres['exitcode_error'].valeur) @@ -1068,13 +1062,13 @@ def clean_string(data, filename=None, content_type=None, policy=None, logfile=No # fichier INI: if options.nouv_politique != '': pol.ecrire_fichier(options.nouv_politique) - print _('Politique sauvee dans le fichier %s') % options.nouv_politique + print(_('Politique sauvee dans le fichier %s') % options.nouv_politique) sys.exit() # si l'option export est active on exporte la politique dans un fichier HTML: if options.export_html != '': pol.ecrire_html(options.export_html) - print _('Politique exportee dans le fichier %s') % options.export_html + print(_('Politique exportee dans le fichier %s') % options.export_html) sys.exit() # check force_extension option (-f): diff --git a/ExeFilter_GUI.py b/ExeFilter_GUI.py index 186a248..a87cd2e 100644 --- a/ExeFilter_GUI.py +++ b/ExeFilter_GUI.py @@ -1,586 +1,586 @@ -#!/usr/bin/env python -# -*- coding: iso-8859-1 -*- -#============================================================================== -""" -ExeFilter GUI - -A Graphical User Interface for ExeFilter, using wxPython. - -This file is part of the ExeFilter project: -U{http://www.decalage.info/exefilter} - -@author: U{Philippe Lagadec} - -@copyright: Philippe Lagadec 2010-2011 - -@license: CeCILL (open-source compatible GPL) - see file LICENCE.txt for the full license - -@version: 0.03 - -@status: alpha -""" -#============================================================================== - -__docformat__ = 'epytext en' - -__author__ = "Philippe Lagadec" -__date__ = "2011-04-30" -__version__ = "0.03" - -#------------------------------------------------------------------------------ -# LICENCE pour le projet ExeFilter: - -# Copyright DGA/CELAR 2004-2008 -# Copyright NATO/NC3A 2008-2010 (modifications PL apres ExeFilter v1.1.0) -# Copyright Philippe Lagadec 2010-2011 -# Auteurs: -# - Philippe Lagadec (PL) - philippe.lagadec(a)laposte.net -# - Arnaud Kerréneur (AK) - arnaud.kerreneur(a)dga.defense.gouv.fr -# - Tanguy Vinceleux (TV) - tanguy.vinceleux(a)dga.defense.gouv.fr -# -# Ce logiciel est régi par la licence CeCILL soumise au droit français et -# respectant les principes de diffusion des logiciels libres. Vous pouvez -# utiliser, modifier et/ou redistribuer ce programme sous les conditions -# de la licence CeCILL telle que diffusée par le CEA, le CNRS et l'INRIA -# sur le site "http://www.cecill.info". Une copie de cette licence est jointe -# dans les fichiers Licence_CeCILL_V2-fr.html et Licence_CeCILL_V2-en.html. -# -# En contrepartie de l'accessibilité au code source et des droits de copie, -# de modification et de redistribution accordés par cette licence, il n'est -# offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, -# seule une responsabilité restreinte pèse sur l'auteur du programme, le -# titulaire des droits patrimoniaux et les concédants successifs. -# -# A cet égard l'attention de l'utilisateur est attirée sur les risques -# associés au chargement, à l'utilisation, à la modification et/ou au -# développement et à la reproduction du logiciel par l'utilisateur étant -# donné sa spécificité de logiciel libre, qui peut le rendre complexe à -# manipuler et qui le réserve donc à des développeurs et des professionnels -# avertis possédant des connaissances informatiques approfondies. Les -# utilisateurs sont donc invités à charger et tester l'adéquation du -# logiciel à leurs besoins dans des conditions permettant d'assurer la -# sécurité de leurs systèmes et ou de leurs données et, plus généralement, -# à l'utiliser et l'exploiter dans les mêmes conditions de sécurité. -# -# Le fait que vous puissiez accéder à cet en-tête signifie que vous avez -# pris connaissance de la licence CeCILL, et que vous en avez accepté les -# termes. - - -#------------------------------------------------------------------------------ -# CHANGELOG: -# generated by wxGlade 0.6.3 on Wed Dec 29 07:28:18 2010 -# 2011-01-24 v0.01 PL: - first working version -# 2011-02-03 v0.02 PL: - fixed main bugs, minimal functionality -# 2011-04-30 v0.03 PL: - added scan mode launched by scan button - - -#--- TODO --------------------------------------------------------------------- -# + file/load policy, save report as HTML/XML -# - catch and display exceptions -# - test with FR locale -# - test with accents -# - optimize similar code in scan and clean functions - -#LATER: -# + portable way to open doc -# + help/link to websites -# + command line options to directly scan or clean a file then show results in -# GUI -# + CLI option to open the cleaned file afterwards, with confirmation popup if -# cleaned, blocked or error -# + use status bar or remove it? -# + set all tooltips -# - option to show logs? (and then hide console) -# + py2exe version, with ruby2exe for origami: -# http://www.decalage.info/en/python/py2exe -# http://www.erikveen.dds.nl/rubyscript2exe/ -# + list: use icons instead of background color? -# + list: white instead of green? -# + error msg box if wx can't be imported or if an error happens in the main app -# loop, using easygui or tkinter -# - progress dialog with % => use a separate thread for transfert -# - populate list during scan -# + list: autosize, but according to window -# + list: possible to wrap long lines? => use ultimatelistctrl from wx.lib.agw -# see http://xoomer.virgilio.it/infinity77/AGW_Docs/index.html -# BUT check first if it works on Linux/Mac! -# + list: fix resultat to have shorter columns: -# - recognized type(s) -# - type according to extension -# - type according to header -# - active content -# - show paths as relative to home dir? -# - default destination path in home dir/exefilter_output? -# - button to open dest dir -# - double-click on a file to open it (with confirmation) -# - builtin text/hex viewer? -# - show type from ext, magic, md5/sha1 (optional) -# - link to policy editor -# - combobox+button to choose/load policy (or just menu?) -# - save combobox values -# - alternate view with a treectrl: using colors and icons to show the tree -# structure. selecting a node shows more details on the right. -# OR could also use wx.gizmos.TreeListCtrl to have both tree and list -# (see c:\Program Files\wxPython2.8 Docs and Demos\demo\TreeListCtrl.py) -# - use specific icons for each filetype, with a (background?) color depending -# on the filter results. (issue when several filters apply) - -#=== IMPORTS ================================================================== - -import sys, os, os.path, traceback - -import wx -import wx.grid - -import ExeFilter as xf -import Rapport, Resultat, Journal - -#=== CONSTANTS ================================================================ - -COLOR_CLEAN = wx.Colour(red=200, green=255, blue=200) # light green -COLOR_CLEANED = wx.Colour(red=255, green=255, blue=175) # light yellow -COLOR_ERROR = wx.Colour(red=255, green=240, blue=175) # light orange -COLOR_BLOCKED = wx.Colour(red=255, green=175, blue=175) # light red - - -#=== INIT ===================================================================== - -# init console logging (after setting the debug mode if -v option): -Journal.init_console_logging() - - -# begin wxGlade: extracode -# end wxGlade - - -#=== CLASSES ================================================================== - -class MainFrame(wx.Frame): - def __init__(self, *args, **kwds): - # begin wxGlade: MainFrame.__init__ - kwds["style"] = wx.DEFAULT_FRAME_STYLE - wx.Frame.__init__(self, *args, **kwds) - - # Menu Bar - self.main_frame_menubar = wx.MenuBar() - global ID_DOC; ID_DOC = wx.NewId() - wxglade_tmp_menu = wx.Menu() - wxglade_tmp_menu.Append(wx.ID_EXIT, "Exit", "", wx.ITEM_NORMAL) - self.main_frame_menubar.Append(wxglade_tmp_menu, "File") - wxglade_tmp_menu = wx.Menu() - wxglade_tmp_menu.Append(wx.ID_ABOUT, "About", "", wx.ITEM_NORMAL) - wxglade_tmp_menu.Append(ID_DOC, "Documentation", "", wx.ITEM_NORMAL) - self.main_frame_menubar.Append(wxglade_tmp_menu, "Help") - self.SetMenuBar(self.main_frame_menubar) - # Menu Bar end - self.label_source = wx.StaticText(self, -1, "Source:") - self.combo_box_source = wx.ComboBox(self, -1, choices=["demo_files"], style=wx.CB_DROPDOWN) - self.button_source_file = wx.Button(self, -1, "Select File...") - self.button_source_folder = wx.Button(self, -1, "Select Folder...") - self.label_dest = wx.StaticText(self, -1, "Destination:") - self.combo_box_dest = wx.ComboBox(self, -1, choices=["demo_output"], style=wx.CB_DROPDOWN) - self.button_dest_file = wx.Button(self, -1, "Select File...") - self.button_dest_folder = wx.Button(self, -1, "Select Folder...") - self.button_scan = wx.Button(self, -1, "Scan") - self.button_clean = wx.Button(self, -1, "Scan and Clean") - self.grid_results = wx.grid.Grid(self, -1, size=(1, 1)) - self.list_results = wx.ListCtrl(self, -1, style=wx.LC_REPORT|wx.LC_HRULES|wx.LC_VRULES|wx.SUNKEN_BORDER) - - self.__set_properties() - self.__do_layout() - - self.Bind(wx.EVT_MENU, self.quit, id=wx.ID_EXIT) - self.Bind(wx.EVT_MENU, self.about, id=wx.ID_ABOUT) - self.Bind(wx.EVT_MENU, self.display_doc, id=ID_DOC) - self.Bind(wx.EVT_BUTTON, self.select_source_file, self.button_source_file) - self.Bind(wx.EVT_BUTTON, self.select_source_dir, self.button_source_folder) - self.Bind(wx.EVT_BUTTON, self.select_dest_file, self.button_dest_file) - self.Bind(wx.EVT_BUTTON, self.select_dest_dir, self.button_dest_folder) - self.Bind(wx.EVT_BUTTON, self.scan, self.button_scan) - self.Bind(wx.EVT_BUTTON, self.clean, self.button_clean) - # end wxGlade - - # disabled grid -## self.grid_results = wx.grid.Grid(self, -1, size=(1, 1)) - - - # set list columns - self.list_results.InsertColumn(col=0, heading='File') - self.list_results.InsertColumn(col=1, heading='Result') - self.list_results.InsertColumn(col=2, heading='Details') - -## self.clear_grid() - - def __set_properties(self): - # begin wxGlade: MainFrame.__set_properties - self.SetTitle("ExeFilter") - self.SetSize((600, 501)) - self.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_3DFACE)) - self.combo_box_source.SetSelection(0) - self.combo_box_dest.SetSelection(0) - self.button_scan.SetToolTipString("Analyze the source file or folder, without cleaning") - self.button_clean.SetToolTipString("Analyze the source file or folder, copy a sanitized version to the destination folder") - self.grid_results.CreateGrid(10, 3) - self.grid_results.SetRowLabelSize(0) - self.grid_results.EnableEditing(0) - self.grid_results.EnableDragRowSize(0) - self.grid_results.EnableDragGridSize(0) - self.grid_results.SetSelectionMode(wx.grid.Grid.wxGridSelectRows) - self.grid_results.SetColLabelValue(0, "File") - self.grid_results.SetColLabelValue(1, "Result") - self.grid_results.SetColLabelValue(2, "Details") - self.grid_results.Enable(False) - self.grid_results.Hide() - # end wxGlade - - # disabled grid -## self.grid_results.CreateGrid(10, 3) -## self.grid_results.SetRowLabelSize(0) -## self.grid_results.EnableEditing(0) -## self.grid_results.EnableDragRowSize(0) -## self.grid_results.EnableDragGridSize(0) -## self.grid_results.SetSelectionMode(wx.grid.Grid.wxGridSelectRows) -## self.grid_results.SetColLabelValue(0, "File") -## self.grid_results.SetColLabelValue(1, "Result") -## self.grid_results.SetColLabelValue(2, "Details") - - - def __do_layout(self): - # begin wxGlade: MainFrame.__do_layout - sizer_vertical = wx.BoxSizer(wx.VERTICAL) - sizer_3 = wx.BoxSizer(wx.HORIZONTAL) - grid_sizer_2 = wx.FlexGridSizer(2, 4, 0, 0) - grid_sizer_2.Add(self.label_source, 0, wx.ALL|wx.EXPAND|wx.ALIGN_CENTER_VERTICAL, 2) - grid_sizer_2.Add(self.combo_box_source, 1, wx.EXPAND, 0) - grid_sizer_2.Add(self.button_source_file, 0, 0, 0) - grid_sizer_2.Add(self.button_source_folder, 0, 0, 0) - grid_sizer_2.Add(self.label_dest, 0, wx.EXPAND|wx.ALIGN_CENTER_VERTICAL, 2) - grid_sizer_2.Add(self.combo_box_dest, 1, wx.EXPAND, 0) - grid_sizer_2.Add(self.button_dest_file, 0, 0, 0) - grid_sizer_2.Add(self.button_dest_folder, 0, 0, 0) - grid_sizer_2.AddGrowableCol(1) - sizer_vertical.Add(grid_sizer_2, 0, wx.ALL|wx.EXPAND, 4) - sizer_3.Add(self.button_scan, 0, wx.ALIGN_CENTER_HORIZONTAL, 0) - sizer_3.Add((20, 20), 0, 0, 0) - sizer_3.Add(self.button_clean, 0, 0, 0) - sizer_vertical.Add(sizer_3, 0, wx.ALL|wx.ALIGN_CENTER_HORIZONTAL, 4) - sizer_vertical.Add(self.grid_results, 0, wx.EXPAND, 0) - sizer_vertical.Add(self.list_results, 1, wx.EXPAND, 0) - self.SetSizer(sizer_vertical) - self.Layout() - # end wxGlade - - # disabled grid -## sizer_vertical.Add(self.grid_results, 1, wx.EXPAND, 0) - - - -## def clear_grid(self): -## """ -## clear the grid data -## """ -## # delete all rows: -## n = self.grid_results.GetNumberRows() -## if n: -## self.grid_results.DeleteRows(0,n) - - - def select_source_file(self, event): # wxGlade: MainFrame. - prev_source = os.path.abspath(self.get_source()) - if os.path.exists(prev_source) and os.path.isdir(prev_source): - # previous source was a dir, use it as starting point: - prev_path = prev_source - prev_source = '' - else: - # previous source was a file or does not exist, use its dir as starting point: - prev_path = os.path.dirname(prev_source) - source = wx.FileSelector(message='Select source file to be scanned', - default_path=prev_path, default_filename=prev_source, - flags=wx.OPEN|wx.FILE_MUST_EXIST) - if source: - self.set_source(source) - - - def select_source_dir(self, event): # wxGlade: MainFrame. - prev_source = os.path.abspath(self.get_source()) - if os.path.exists(prev_source) and os.path.isfile(prev_source): - # previous source was a file, use its dir as starting point: - prev_source = os.path.dirname(prev_source) - source_dir = wx.DirSelector(message='Select source folder to be scanned', - defaultPath=prev_source) - if source_dir: - self.set_source(source_dir) - - - def select_dest_file(self, event): # wxGlade: MainFrame. - prev_dest = os.path.abspath(self.get_dest()) - if os.path.exists(prev_dest) and os.path.isdir(prev_dest): - # previous dest was a dir, use it as starting point: - prev_path = prev_dest - # filename = same as source - prev_dest = os.path.basename(self.get_source()) - else: - # previous dest was a file or does not exist, use its dir as starting point: - prev_path = os.path.dirname(prev_dest) - dest = wx.FileSelector(message='Enter the name of the destination file', - default_path=prev_path, default_filename=prev_dest, - flags=wx.SAVE|wx.OVERWRITE_PROMPT) - if dest: - self.set_dest(dest) -## fileDialog = wx.FileDialog(self, message='Enter the name of the destination file', -## defaultDir=self.get_dest(), defaultFile='', style=wx.SAVE|wx.OVERWRITE_PROMPT) -## result = fileDialog.ShowModal() -## if result == wx.ID_OK: -## #TODO: convert to relative path? -## self.dest_file = fileDialog.GetPath() -## #wx.LogMessage('You selected: %s\n' % self.source_file) -## self.set_dest(self.dest_file) -## fileDialog.Destroy() - - - def select_dest_dir(self, event): # wxGlade: MainFrame. - prev_dest = os.path.abspath(self.get_dest()) - if os.path.exists(prev_dest) and os.path.isfile(prev_dest): - # previous dest was a file, use its dir as starting point: - prev_dest = os.path.dirname(prev_dest) - dest_dir = wx.DirSelector(message='Select destination folder for cleaned files', - defaultPath=prev_dest) - if dest_dir: - self.set_dest(dest_dir) - - - def get_source(self): - return self.combo_box_source.GetValue() - - def get_dest(self): - return self.combo_box_dest.GetValue() - - - def set_source(self, source_path): - self.combo_box_source.SetValue(source_path) - - def set_dest(self, dest_path): - self.combo_box_dest.SetValue(dest_path) - - - def clear_results(self): - """ - clear the list of results - """ - # delete all rows: - self.list_results.DeleteAllItems() - - - def scan(self, event): # wxGlade: MainFrame. - source = self.get_source() - if not os.path.exists(source): - wx.MessageBox('Error: the source path does not exist.', 'Error', wx.ICON_ERROR) - return - # delete all rows: - self.clear_results() - d = wx.ProgressDialog(title='Scanning', message='Scanning %s...' % source) - d.Pulse() - try: - if os.path.isdir(source): - wx.BeginBusyCursor() - xf.scan_dir(source) - wx.EndBusyCursor() - else: - wx.BeginBusyCursor() - xf.scan_file(source) - wx.EndBusyCursor() - #xf.display_html_report() - except: - wx.EndBusyCursor() - msg = 'Unhandled error while cleaning: please report it to decalage(a)laposte.net\n' + traceback.format_exc() - wx.MessageBox(msg, 'Error', wx.ICON_ERROR) - d.Update(value=100) - # it's necessary to destroy the dialog else the console stays open at the end - d.Destroy() - l = self.list_results - for result in Rapport.liste_resultats: - filename = result.chemin_fichier - index = l.InsertStringItem(sys.maxint, filename) - if result.code_resultat == Resultat.ACCEPTE: - text = 'CLEAN' - color = COLOR_CLEAN #wx.TheColourDatabase.FindColour('GREEN') #wx.GREEN - elif result.code_resultat == Resultat.NETTOYE: - text = 'TO BE CLEANED' - color = COLOR_CLEANED #wx.TheColourDatabase.FindColour('YELLOW') #wx.YELLOW - elif result.code_resultat == Resultat.REFUSE \ - or result.code_resultat == Resultat.EXT_NON_AUTORISEE \ - or result.code_resultat == Resultat.FORMAT_INCORRECT \ - or result.code_resultat == Resultat.VIRUS : - text = 'NOT ALLOWED' - color = COLOR_BLOCKED #wx.TheColourDatabase.FindColour('RED') #wx.RED - elif result.code_resultat == Resultat.ERREUR_LECTURE \ - or result.code_resultat == Resultat.ERREUR_ANALYSE: - text = 'ERROR' - color = COLOR_ERROR #wx.TheColourDatabase.FindColour('ORANGE') - l.SetStringItem(index, 1, text) - if result.code_resultat == Resultat.EXT_NON_AUTORISEE: - details = result.details() + '\n' - else: - details = '' - details += '\n'.join(result.raison) - l.SetStringItem(index, 2, details) - l.SetItemBackgroundColour(index, color) - for col in range(3): - l.SetColumnWidth(col=col, width=-1) - - - def clean(self, event): # wxGlade: MainFrame. - source = self.get_source() - dest = self.get_dest() - if not os.path.exists(source): - wx.MessageBox('Error: the source path does not exist.', 'Error', wx.ICON_ERROR) - return - # delete all rows: - self.clear_results() - d = wx.ProgressDialog(title='Cleaning', message='Scanning and cleaning %s\nto %s...' - % (source, dest)) - d.Pulse() - try: - if os.path.isdir(source): - # dest must also be a dir: - if os.path.exists(dest): - if not os.path.isdir(dest): - wx.MessageBox('Error: if the source is a folder, the destination must also be a folder.', 'Error', wx.ICON_ERROR) - return - # else dest is created as a dir if it doesn't exist - wx.BeginBusyCursor() - xf.transfert([source], dest) - wx.EndBusyCursor() - else: - wx.BeginBusyCursor() - xf.transfert([source], dest, dest_is_a_file=True) - wx.EndBusyCursor() - #xf.display_html_report() - except: - wx.EndBusyCursor() - msg = 'Unhandled error while cleaning: please report it to decalage(a)laposte.net\n' + traceback.format_exc() - wx.MessageBox(msg, 'Error', wx.ICON_ERROR) - d.Update(value=100) - # it's necessary to destroy the dialog else the console stays open at the end - d.Destroy() - l = self.list_results - for result in Rapport.liste_resultats: - filename = result.chemin_fichier - index = l.InsertStringItem(sys.maxint, filename) - if result.code_resultat == Resultat.ACCEPTE: - text = 'CLEAN' - color = COLOR_CLEAN #wx.TheColourDatabase.FindColour('GREEN') #wx.GREEN - elif result.code_resultat == Resultat.NETTOYE: - text = 'CLEANED' - color = COLOR_CLEANED #wx.TheColourDatabase.FindColour('YELLOW') #wx.YELLOW - elif result.code_resultat == Resultat.REFUSE \ - or result.code_resultat == Resultat.EXT_NON_AUTORISEE \ - or result.code_resultat == Resultat.FORMAT_INCORRECT \ - or result.code_resultat == Resultat.VIRUS : - text = 'BLOCKED' - color = COLOR_BLOCKED #wx.TheColourDatabase.FindColour('RED') #wx.RED - elif result.code_resultat == Resultat.ERREUR_LECTURE \ - or result.code_resultat == Resultat.ERREUR_ANALYSE: - text = 'ERROR' - color = COLOR_ERROR #wx.TheColourDatabase.FindColour('ORANGE') - l.SetStringItem(index, 1, text) - if result.code_resultat == Resultat.EXT_NON_AUTORISEE: - details = result.details() + '\n' - else: - details = '' - details += '\n'.join(result.raison) - l.SetStringItem(index, 2, details) - l.SetItemBackgroundColour(index, color) - for col in range(3): - l.SetColumnWidth(col=col, width=-1) - - # old code with grid widget: -## grid = self.grid_results -## # delete all rows: -## n = grid.GetNumberRows() -## if n: -## grid.DeleteRows(0,n) -## row = 0 -## for result in Rapport.liste_resultats: -## grid.AppendRows(1) -## filename = result.chemin_fichier -## grid.SetCellValue(row, 0, filename) -## if result.code_resultat == Resultat.ACCEPTE: -## text = 'CLEAN' -## color = wx.TheColourDatabase.FindColour('GREEN') #wx.GREEN -## elif result.code_resultat == Resultat.NETTOYE: -## text = 'CLEANED' -## color = wx.TheColourDatabase.FindColour('YELLOW') #wx.YELLOW -## elif result.code_resultat == Resultat.REFUSE \ -## or result.code_resultat == Resultat.EXT_NON_AUTORISEE \ -## or result.code_resultat == Resultat.FORMAT_INCORRECT \ -## or result.code_resultat == Resultat.VIRUS : -## text = 'BLOCKED' -## color = wx.TheColourDatabase.FindColour('RED') #wx.RED -## elif result.code_resultat == Resultat.ERREUR_LECTURE \ -## or result.code_resultat == Resultat.ERREUR_ANALYSE: -## text = 'ERROR' -## color = wx.TheColourDatabase.FindColour('ORANGE') -## grid.SetCellValue(row, 1, text) -## grid.SetCellBackgroundColour(row, 1, color) -## if result.code_resultat == Resultat.EXT_NON_AUTORISEE: -## details = result.details() + '\n' -## else: -## details = '' -## details += '\n'.join(result.raison) -## details = details.strip() -## grid.SetCellValue(row, 2, details) -## row += 1 -## grid.AutoSize() -## grid.ForceRefresh() - - def quit(self, event): # wxGlade: MainFrame. - self.Close(True) - - def load_policy(self, event): # wxGlade: MainFrame. - print "Event handler `load_policy' not implemented" - event.Skip() - - def report_html(self, event): # wxGlade: MainFrame. - print "Event handler `report_html' not implemented" - event.Skip() - - def report_xml(self, event): # wxGlade: MainFrame. - print "Event handler `report_xml' not implemented" - event.Skip() - - def display_doc(self, event): # wxGlade: MainFrame. - os.startfile('ExeFilter_documentation_EN.pdf') - # on most Linux systems: - #os.system("/usr/bin/xdg-open ExeFilter_documentation_EN.pdf") - # on MacOSX: - #os.system("/usr/bin/open ExeFilter_documentation_EN.pdf") - # last alternative: open file URL in web browser? - - def about(self, event): # wxGlade: MainFrame. - info = wx.AboutDialogInfo() - info.SetName('ExeFilter GUI') - info.SetVersion(xf.XF_VERSION) - #info.SetDevelopers([__author__]) - info.SetDescription('A tool to scan and sanitize files, removing active content in common file formats ' - 'such as PDF, HTML, RTF and MS Office.') - info.SetCopyright('Copyright (C) Philippe Lagadec 2010-2011') - info.SetWebSite('http://www.decalage.info/exefilter') - info.SetLicense('CeCILL license, open-source, GPL-compatible.\n' - 'See file Licence_CeCILL_V2-en.html for the full license.') - wx.AboutBox(info) - -# end of class MainFrame - - -#=== MAIN ===================================================================== - -if __name__ == "__main__": - xfgui_app = wx.PySimpleApp(0) - wx.InitAllImageHandlers() - main_frame = MainFrame(None, -1, "") - xfgui_app.SetTopWindow(main_frame) - main_frame.Show() - xfgui_app.MainLoop() +#!/usr/bin/env python +# -*- coding: iso-8859-1 -*- +#============================================================================== +""" +ExeFilter GUI + +A Graphical User Interface for ExeFilter, using wxPython. + +This file is part of the ExeFilter project: +U{http://www.decalage.info/exefilter} + +@author: U{Philippe Lagadec} + +@copyright: Philippe Lagadec 2010-2011 + +@license: CeCILL (open-source compatible GPL) + see file LICENCE.txt for the full license + +@version: 0.03 + +@status: alpha +""" +#============================================================================== + +__docformat__ = 'epytext en' + +__author__ = "Philippe Lagadec" +__date__ = "2011-04-30" +__version__ = "0.03" + +#------------------------------------------------------------------------------ +# LICENCE pour le projet ExeFilter: + +# Copyright DGA/CELAR 2004-2008 +# Copyright NATO/NC3A 2008-2010 (modifications PL apres ExeFilter v1.1.0) +# Copyright Philippe Lagadec 2010-2011 +# Auteurs: +# - Philippe Lagadec (PL) - philippe.lagadec(a)laposte.net +# - Arnaud Kerréneur (AK) - arnaud.kerreneur(a)dga.defense.gouv.fr +# - Tanguy Vinceleux (TV) - tanguy.vinceleux(a)dga.defense.gouv.fr +# +# Ce logiciel est régi par la licence CeCILL soumise au droit français et +# respectant les principes de diffusion des logiciels libres. Vous pouvez +# utiliser, modifier et/ou redistribuer ce programme sous les conditions +# de la licence CeCILL telle que diffusée par le CEA, le CNRS et l'INRIA +# sur le site "http://www.cecill.info". Une copie de cette licence est jointe +# dans les fichiers Licence_CeCILL_V2-fr.html et Licence_CeCILL_V2-en.html. +# +# En contrepartie de l'accessibilité au code source et des droits de copie, +# de modification et de redistribution accordés par cette licence, il n'est +# offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, +# seule une responsabilité restreinte pèse sur l'auteur du programme, le +# titulaire des droits patrimoniaux et les concédants successifs. +# +# A cet égard l'attention de l'utilisateur est attirée sur les risques +# associés au chargement, à l'utilisation, à la modification et/ou au +# développement et à la reproduction du logiciel par l'utilisateur étant +# donné sa spécificité de logiciel libre, qui peut le rendre complexe à +# manipuler et qui le réserve donc à des développeurs et des professionnels +# avertis possédant des connaissances informatiques approfondies. Les +# utilisateurs sont donc invités à charger et tester l'adéquation du +# logiciel à leurs besoins dans des conditions permettant d'assurer la +# sécurité de leurs systèmes et ou de leurs données et, plus généralement, +# à l'utiliser et l'exploiter dans les mêmes conditions de sécurité. +# +# Le fait que vous puissiez accéder à cet en-tête signifie que vous avez +# pris connaissance de la licence CeCILL, et que vous en avez accepté les +# termes. + + +#------------------------------------------------------------------------------ +# CHANGELOG: +# generated by wxGlade 0.6.3 on Wed Dec 29 07:28:18 2010 +# 2011-01-24 v0.01 PL: - first working version +# 2011-02-03 v0.02 PL: - fixed main bugs, minimal functionality +# 2011-04-30 v0.03 PL: - added scan mode launched by scan button + + +#--- TODO --------------------------------------------------------------------- +# + file/load policy, save report as HTML/XML +# - catch and display exceptions +# - test with FR locale +# - test with accents +# - optimize similar code in scan and clean functions + +#LATER: +# + portable way to open doc +# + help/link to websites +# + command line options to directly scan or clean a file then show results in +# GUI +# + CLI option to open the cleaned file afterwards, with confirmation popup if +# cleaned, blocked or error +# + use status bar or remove it? +# + set all tooltips +# - option to show logs? (and then hide console) +# + py2exe version, with ruby2exe for origami: +# http://www.decalage.info/en/python/py2exe +# http://www.erikveen.dds.nl/rubyscript2exe/ +# + list: use icons instead of background color? +# + list: white instead of green? +# + error msg box if wx can't be imported or if an error happens in the main app +# loop, using easygui or tkinter +# - progress dialog with % => use a separate thread for transfert +# - populate list during scan +# + list: autosize, but according to window +# + list: possible to wrap long lines? => use ultimatelistctrl from wx.lib.agw +# see http://xoomer.virgilio.it/infinity77/AGW_Docs/index.html +# BUT check first if it works on Linux/Mac! +# + list: fix resultat to have shorter columns: +# - recognized type(s) +# - type according to extension +# - type according to header +# - active content +# - show paths as relative to home dir? +# - default destination path in home dir/exefilter_output? +# - button to open dest dir +# - double-click on a file to open it (with confirmation) +# - builtin text/hex viewer? +# - show type from ext, magic, md5/sha1 (optional) +# - link to policy editor +# - combobox+button to choose/load policy (or just menu?) +# - save combobox values +# - alternate view with a treectrl: using colors and icons to show the tree +# structure. selecting a node shows more details on the right. +# OR could also use wx.gizmos.TreeListCtrl to have both tree and list +# (see c:\Program Files\wxPython2.8 Docs and Demos\demo\TreeListCtrl.py) +# - use specific icons for each filetype, with a (background?) color depending +# on the filter results. (issue when several filters apply) + +#=== IMPORTS ================================================================== + +import sys, os, os.path, traceback + +import wx +import wx.grid + +import ExeFilter as xf +import Rapport, Resultat, Journal + +#=== CONSTANTS ================================================================ + +COLOR_CLEAN = wx.Colour(red=200, green=255, blue=200) # light green +COLOR_CLEANED = wx.Colour(red=255, green=255, blue=175) # light yellow +COLOR_ERROR = wx.Colour(red=255, green=240, blue=175) # light orange +COLOR_BLOCKED = wx.Colour(red=255, green=175, blue=175) # light red + + +#=== INIT ===================================================================== + +# init console logging (after setting the debug mode if -v option): +Journal.init_console_logging() + + +# begin wxGlade: extracode +# end wxGlade + + +#=== CLASSES ================================================================== + +class MainFrame(wx.Frame): + def __init__(self, *args, **kwds): + # begin wxGlade: MainFrame.__init__ + kwds["style"] = wx.DEFAULT_FRAME_STYLE + wx.Frame.__init__(self, *args, **kwds) + + # Menu Bar + self.main_frame_menubar = wx.MenuBar() + global ID_DOC; ID_DOC = wx.NewId() + wxglade_tmp_menu = wx.Menu() + wxglade_tmp_menu.Append(wx.ID_EXIT, "Exit", "", wx.ITEM_NORMAL) + self.main_frame_menubar.Append(wxglade_tmp_menu, "File") + wxglade_tmp_menu = wx.Menu() + wxglade_tmp_menu.Append(wx.ID_ABOUT, "About", "", wx.ITEM_NORMAL) + wxglade_tmp_menu.Append(ID_DOC, "Documentation", "", wx.ITEM_NORMAL) + self.main_frame_menubar.Append(wxglade_tmp_menu, "Help") + self.SetMenuBar(self.main_frame_menubar) + # Menu Bar end + self.label_source = wx.StaticText(self, -1, "Source:") + self.combo_box_source = wx.ComboBox(self, -1, choices=["demo_files"], style=wx.CB_DROPDOWN) + self.button_source_file = wx.Button(self, -1, "Select File...") + self.button_source_folder = wx.Button(self, -1, "Select Folder...") + self.label_dest = wx.StaticText(self, -1, "Destination:") + self.combo_box_dest = wx.ComboBox(self, -1, choices=["demo_output"], style=wx.CB_DROPDOWN) + self.button_dest_file = wx.Button(self, -1, "Select File...") + self.button_dest_folder = wx.Button(self, -1, "Select Folder...") + self.button_scan = wx.Button(self, -1, "Scan") + self.button_clean = wx.Button(self, -1, "Scan and Clean") + self.grid_results = wx.grid.Grid(self, -1, size=(1, 1)) + self.list_results = wx.ListCtrl(self, -1, style=wx.LC_REPORT|wx.LC_HRULES|wx.LC_VRULES|wx.SUNKEN_BORDER) + + self.__set_properties() + self.__do_layout() + + self.Bind(wx.EVT_MENU, self.quit, id=wx.ID_EXIT) + self.Bind(wx.EVT_MENU, self.about, id=wx.ID_ABOUT) + self.Bind(wx.EVT_MENU, self.display_doc, id=ID_DOC) + self.Bind(wx.EVT_BUTTON, self.select_source_file, self.button_source_file) + self.Bind(wx.EVT_BUTTON, self.select_source_dir, self.button_source_folder) + self.Bind(wx.EVT_BUTTON, self.select_dest_file, self.button_dest_file) + self.Bind(wx.EVT_BUTTON, self.select_dest_dir, self.button_dest_folder) + self.Bind(wx.EVT_BUTTON, self.scan, self.button_scan) + self.Bind(wx.EVT_BUTTON, self.clean, self.button_clean) + # end wxGlade + + # disabled grid +## self.grid_results = wx.grid.Grid(self, -1, size=(1, 1)) + + + # set list columns + self.list_results.InsertColumn(col=0, heading='File') + self.list_results.InsertColumn(col=1, heading='Result') + self.list_results.InsertColumn(col=2, heading='Details') + +## self.clear_grid() + + def __set_properties(self): + # begin wxGlade: MainFrame.__set_properties + self.SetTitle("ExeFilter") + self.SetSize((600, 501)) + self.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_3DFACE)) + self.combo_box_source.SetSelection(0) + self.combo_box_dest.SetSelection(0) + self.button_scan.SetToolTipString("Analyze the source file or folder, without cleaning") + self.button_clean.SetToolTipString("Analyze the source file or folder, copy a sanitized version to the destination folder") + self.grid_results.CreateGrid(10, 3) + self.grid_results.SetRowLabelSize(0) + self.grid_results.EnableEditing(0) + self.grid_results.EnableDragRowSize(0) + self.grid_results.EnableDragGridSize(0) + self.grid_results.SetSelectionMode(wx.grid.Grid.wxGridSelectRows) + self.grid_results.SetColLabelValue(0, "File") + self.grid_results.SetColLabelValue(1, "Result") + self.grid_results.SetColLabelValue(2, "Details") + self.grid_results.Enable(False) + self.grid_results.Hide() + # end wxGlade + + # disabled grid +## self.grid_results.CreateGrid(10, 3) +## self.grid_results.SetRowLabelSize(0) +## self.grid_results.EnableEditing(0) +## self.grid_results.EnableDragRowSize(0) +## self.grid_results.EnableDragGridSize(0) +## self.grid_results.SetSelectionMode(wx.grid.Grid.wxGridSelectRows) +## self.grid_results.SetColLabelValue(0, "File") +## self.grid_results.SetColLabelValue(1, "Result") +## self.grid_results.SetColLabelValue(2, "Details") + + + def __do_layout(self): + # begin wxGlade: MainFrame.__do_layout + sizer_vertical = wx.BoxSizer(wx.VERTICAL) + sizer_3 = wx.BoxSizer(wx.HORIZONTAL) + grid_sizer_2 = wx.FlexGridSizer(2, 4, 0, 0) + grid_sizer_2.Add(self.label_source, 0, wx.ALL|wx.EXPAND|wx.ALIGN_CENTER_VERTICAL, 2) + grid_sizer_2.Add(self.combo_box_source, 1, wx.EXPAND, 0) + grid_sizer_2.Add(self.button_source_file, 0, 0, 0) + grid_sizer_2.Add(self.button_source_folder, 0, 0, 0) + grid_sizer_2.Add(self.label_dest, 0, wx.EXPAND|wx.ALIGN_CENTER_VERTICAL, 2) + grid_sizer_2.Add(self.combo_box_dest, 1, wx.EXPAND, 0) + grid_sizer_2.Add(self.button_dest_file, 0, 0, 0) + grid_sizer_2.Add(self.button_dest_folder, 0, 0, 0) + grid_sizer_2.AddGrowableCol(1) + sizer_vertical.Add(grid_sizer_2, 0, wx.ALL|wx.EXPAND, 4) + sizer_3.Add(self.button_scan, 0, wx.ALIGN_CENTER_HORIZONTAL, 0) + sizer_3.Add((20, 20), 0, 0, 0) + sizer_3.Add(self.button_clean, 0, 0, 0) + sizer_vertical.Add(sizer_3, 0, wx.ALL|wx.ALIGN_CENTER_HORIZONTAL, 4) + sizer_vertical.Add(self.grid_results, 0, wx.EXPAND, 0) + sizer_vertical.Add(self.list_results, 1, wx.EXPAND, 0) + self.SetSizer(sizer_vertical) + self.Layout() + # end wxGlade + + # disabled grid +## sizer_vertical.Add(self.grid_results, 1, wx.EXPAND, 0) + + + +## def clear_grid(self): +## """ +## clear the grid data +## """ +## # delete all rows: +## n = self.grid_results.GetNumberRows() +## if n: +## self.grid_results.DeleteRows(0,n) + + + def select_source_file(self, event): # wxGlade: MainFrame. + prev_source = os.path.abspath(self.get_source()) + if os.path.exists(prev_source) and os.path.isdir(prev_source): + # previous source was a dir, use it as starting point: + prev_path = prev_source + prev_source = '' + else: + # previous source was a file or does not exist, use its dir as starting point: + prev_path = os.path.dirname(prev_source) + source = wx.FileSelector(message='Select source file to be scanned', + default_path=prev_path, default_filename=prev_source, + flags=wx.OPEN|wx.FILE_MUST_EXIST) + if source: + self.set_source(source) + + + def select_source_dir(self, event): # wxGlade: MainFrame. + prev_source = os.path.abspath(self.get_source()) + if os.path.exists(prev_source) and os.path.isfile(prev_source): + # previous source was a file, use its dir as starting point: + prev_source = os.path.dirname(prev_source) + source_dir = wx.DirSelector(message='Select source folder to be scanned', + defaultPath=prev_source) + if source_dir: + self.set_source(source_dir) + + + def select_dest_file(self, event): # wxGlade: MainFrame. + prev_dest = os.path.abspath(self.get_dest()) + if os.path.exists(prev_dest) and os.path.isdir(prev_dest): + # previous dest was a dir, use it as starting point: + prev_path = prev_dest + # filename = same as source + prev_dest = os.path.basename(self.get_source()) + else: + # previous dest was a file or does not exist, use its dir as starting point: + prev_path = os.path.dirname(prev_dest) + dest = wx.FileSelector(message='Enter the name of the destination file', + default_path=prev_path, default_filename=prev_dest, + flags=wx.SAVE|wx.OVERWRITE_PROMPT) + if dest: + self.set_dest(dest) +## fileDialog = wx.FileDialog(self, message='Enter the name of the destination file', +## defaultDir=self.get_dest(), defaultFile='', style=wx.SAVE|wx.OVERWRITE_PROMPT) +## result = fileDialog.ShowModal() +## if result == wx.ID_OK: +## #TODO: convert to relative path? +## self.dest_file = fileDialog.GetPath() +## #wx.LogMessage('You selected: %s\n' % self.source_file) +## self.set_dest(self.dest_file) +## fileDialog.Destroy() + + + def select_dest_dir(self, event): # wxGlade: MainFrame. + prev_dest = os.path.abspath(self.get_dest()) + if os.path.exists(prev_dest) and os.path.isfile(prev_dest): + # previous dest was a file, use its dir as starting point: + prev_dest = os.path.dirname(prev_dest) + dest_dir = wx.DirSelector(message='Select destination folder for cleaned files', + defaultPath=prev_dest) + if dest_dir: + self.set_dest(dest_dir) + + + def get_source(self): + return self.combo_box_source.GetValue() + + def get_dest(self): + return self.combo_box_dest.GetValue() + + + def set_source(self, source_path): + self.combo_box_source.SetValue(source_path) + + def set_dest(self, dest_path): + self.combo_box_dest.SetValue(dest_path) + + + def clear_results(self): + """ + clear the list of results + """ + # delete all rows: + self.list_results.DeleteAllItems() + + + def scan(self, event): # wxGlade: MainFrame. + source = self.get_source() + if not os.path.exists(source): + wx.MessageBox('Error: the source path does not exist.', 'Error', wx.ICON_ERROR) + return + # delete all rows: + self.clear_results() + d = wx.ProgressDialog(title='Scanning', message='Scanning %s...' % source) + d.Pulse() + try: + if os.path.isdir(source): + wx.BeginBusyCursor() + xf.scan_dir(source) + wx.EndBusyCursor() + else: + wx.BeginBusyCursor() + xf.scan_file(source) + wx.EndBusyCursor() + #xf.display_html_report() + except: + wx.EndBusyCursor() + msg = 'Unhandled error while cleaning: please report it to decalage(a)laposte.net\n' + traceback.format_exc() + wx.MessageBox(msg, 'Error', wx.ICON_ERROR) + d.Update(value=100) + # it's necessary to destroy the dialog else the console stays open at the end + d.Destroy() + l = self.list_results + for result in Rapport.liste_resultats: + filename = result.chemin_fichier + index = l.InsertStringItem(sys.maxint, filename) + if result.code_resultat == Resultat.ACCEPTE: + text = 'CLEAN' + color = COLOR_CLEAN #wx.TheColourDatabase.FindColour('GREEN') #wx.GREEN + elif result.code_resultat == Resultat.NETTOYE: + text = 'TO BE CLEANED' + color = COLOR_CLEANED #wx.TheColourDatabase.FindColour('YELLOW') #wx.YELLOW + elif result.code_resultat == Resultat.REFUSE \ + or result.code_resultat == Resultat.EXT_NON_AUTORISEE \ + or result.code_resultat == Resultat.FORMAT_INCORRECT \ + or result.code_resultat == Resultat.VIRUS : + text = 'NOT ALLOWED' + color = COLOR_BLOCKED #wx.TheColourDatabase.FindColour('RED') #wx.RED + elif result.code_resultat == Resultat.ERREUR_LECTURE \ + or result.code_resultat == Resultat.ERREUR_ANALYSE: + text = 'ERROR' + color = COLOR_ERROR #wx.TheColourDatabase.FindColour('ORANGE') + l.SetStringItem(index, 1, text) + if result.code_resultat == Resultat.EXT_NON_AUTORISEE: + details = result.details() + '\n' + else: + details = '' + details += '\n'.join(result.raison) + l.SetStringItem(index, 2, details) + l.SetItemBackgroundColour(index, color) + for col in range(3): + l.SetColumnWidth(col=col, width=-1) + + + def clean(self, event): # wxGlade: MainFrame. + source = self.get_source() + dest = self.get_dest() + if not os.path.exists(source): + wx.MessageBox('Error: the source path does not exist.', 'Error', wx.ICON_ERROR) + return + # delete all rows: + self.clear_results() + d = wx.ProgressDialog(title='Cleaning', message='Scanning and cleaning %s\nto %s...' + % (source, dest)) + d.Pulse() + try: + if os.path.isdir(source): + # dest must also be a dir: + if os.path.exists(dest): + if not os.path.isdir(dest): + wx.MessageBox('Error: if the source is a folder, the destination must also be a folder.', 'Error', wx.ICON_ERROR) + return + # else dest is created as a dir if it doesn't exist + wx.BeginBusyCursor() + xf.transfert([source], dest) + wx.EndBusyCursor() + else: + wx.BeginBusyCursor() + xf.transfert([source], dest, dest_is_a_file=True) + wx.EndBusyCursor() + #xf.display_html_report() + except: + wx.EndBusyCursor() + msg = 'Unhandled error while cleaning: please report it to decalage(a)laposte.net\n' + traceback.format_exc() + wx.MessageBox(msg, 'Error', wx.ICON_ERROR) + d.Update(value=100) + # it's necessary to destroy the dialog else the console stays open at the end + d.Destroy() + l = self.list_results + for result in Rapport.liste_resultats: + filename = result.chemin_fichier + index = l.InsertStringItem(sys.maxint, filename) + if result.code_resultat == Resultat.ACCEPTE: + text = 'CLEAN' + color = COLOR_CLEAN #wx.TheColourDatabase.FindColour('GREEN') #wx.GREEN + elif result.code_resultat == Resultat.NETTOYE: + text = 'CLEANED' + color = COLOR_CLEANED #wx.TheColourDatabase.FindColour('YELLOW') #wx.YELLOW + elif result.code_resultat == Resultat.REFUSE \ + or result.code_resultat == Resultat.EXT_NON_AUTORISEE \ + or result.code_resultat == Resultat.FORMAT_INCORRECT \ + or result.code_resultat == Resultat.VIRUS : + text = 'BLOCKED' + color = COLOR_BLOCKED #wx.TheColourDatabase.FindColour('RED') #wx.RED + elif result.code_resultat == Resultat.ERREUR_LECTURE \ + or result.code_resultat == Resultat.ERREUR_ANALYSE: + text = 'ERROR' + color = COLOR_ERROR #wx.TheColourDatabase.FindColour('ORANGE') + l.SetStringItem(index, 1, text) + if result.code_resultat == Resultat.EXT_NON_AUTORISEE: + details = result.details() + '\n' + else: + details = '' + details += '\n'.join(result.raison) + l.SetStringItem(index, 2, details) + l.SetItemBackgroundColour(index, color) + for col in range(3): + l.SetColumnWidth(col=col, width=-1) + + # old code with grid widget: +## grid = self.grid_results +## # delete all rows: +## n = grid.GetNumberRows() +## if n: +## grid.DeleteRows(0,n) +## row = 0 +## for result in Rapport.liste_resultats: +## grid.AppendRows(1) +## filename = result.chemin_fichier +## grid.SetCellValue(row, 0, filename) +## if result.code_resultat == Resultat.ACCEPTE: +## text = 'CLEAN' +## color = wx.TheColourDatabase.FindColour('GREEN') #wx.GREEN +## elif result.code_resultat == Resultat.NETTOYE: +## text = 'CLEANED' +## color = wx.TheColourDatabase.FindColour('YELLOW') #wx.YELLOW +## elif result.code_resultat == Resultat.REFUSE \ +## or result.code_resultat == Resultat.EXT_NON_AUTORISEE \ +## or result.code_resultat == Resultat.FORMAT_INCORRECT \ +## or result.code_resultat == Resultat.VIRUS : +## text = 'BLOCKED' +## color = wx.TheColourDatabase.FindColour('RED') #wx.RED +## elif result.code_resultat == Resultat.ERREUR_LECTURE \ +## or result.code_resultat == Resultat.ERREUR_ANALYSE: +## text = 'ERROR' +## color = wx.TheColourDatabase.FindColour('ORANGE') +## grid.SetCellValue(row, 1, text) +## grid.SetCellBackgroundColour(row, 1, color) +## if result.code_resultat == Resultat.EXT_NON_AUTORISEE: +## details = result.details() + '\n' +## else: +## details = '' +## details += '\n'.join(result.raison) +## details = details.strip() +## grid.SetCellValue(row, 2, details) +## row += 1 +## grid.AutoSize() +## grid.ForceRefresh() + + def quit(self, event): # wxGlade: MainFrame. + self.Close(True) + + def load_policy(self, event): # wxGlade: MainFrame. + print("Event handler `load_policy' not implemented") + event.Skip() + + def report_html(self, event): # wxGlade: MainFrame. + print("Event handler `report_html' not implemented") + event.Skip() + + def report_xml(self, event): # wxGlade: MainFrame. + print("Event handler `report_xml' not implemented") + event.Skip() + + def display_doc(self, event): # wxGlade: MainFrame. + os.startfile('ExeFilter_documentation_EN.pdf') + # on most Linux systems: + #os.system("/usr/bin/xdg-open ExeFilter_documentation_EN.pdf") + # on MacOSX: + #os.system("/usr/bin/open ExeFilter_documentation_EN.pdf") + # last alternative: open file URL in web browser? + + def about(self, event): # wxGlade: MainFrame. + info = wx.AboutDialogInfo() + info.SetName('ExeFilter GUI') + info.SetVersion(xf.XF_VERSION) + #info.SetDevelopers([__author__]) + info.SetDescription('A tool to scan and sanitize files, removing active content in common file formats ' + 'such as PDF, HTML, RTF and MS Office.') + info.SetCopyright('Copyright (C) Philippe Lagadec 2010-2011') + info.SetWebSite('http://www.decalage.info/exefilter') + info.SetLicense('CeCILL license, open-source, GPL-compatible.\n' + 'See file Licence_CeCILL_V2-en.html for the full license.') + wx.AboutBox(info) + +# end of class MainFrame + + +#=== MAIN ===================================================================== + +if __name__ == "__main__": + xfgui_app = wx.PySimpleApp(0) + wx.InitAllImageHandlers() + main_frame = MainFrame(None, -1, "") + xfgui_app.SetTopWindow(main_frame) + main_frame.Show() + xfgui_app.MainLoop() diff --git a/Fichier.py b/Fichier.py index b201b80..32e62ee 100644 --- a/Fichier.py +++ b/Fichier.py @@ -1,621 +1,619 @@ -#!/usr/bin/python -# -*- coding: iso-8859-1 -*- -""" -Fichier - ExeFilter - -Module qui contient la classe L{Fichier.Fichier}, pour representer un fichier a -analyser. - -Ce fichier fait partie du projet ExeFilter. -URL du projet: U{http://www.decalage.info/exefilter} - -@organization: DGA/CELAR -@author: U{Philippe Lagadec} -@author: U{Arnaud Kerréneur} - -@contact: U{Philippe Lagadec} - -@copyright: DGA/CELAR 2004-2008 -@copyright: NATO/NC3A 2008-2010 (PL changes after ExeFilter v1.1.0) -@license: CeCILL (open-source compatible GPL) - cf. code source ou fichier LICENCE.txt joint - -@version: 1.05 - -@status: beta -""" - -#============================================================================== -__docformat__ = 'epytext en' - -__date__ = "2010-04-20" -__version__ = "1.05" - -#------------------------------------------------------------------------------ -# LICENCE pour le projet ExeFilter: - -# Copyright DGA/CELAR 2004-2008 -# Copyright NATO/NC3A 2008-2010 (PL changes after ExeFilter v1.1.0) -# Auteurs: -# - Philippe Lagadec (PL) - philippe.lagadec(a)laposte.net -# - Arnaud Kerréneur (AK) - arnaud.kerreneur(a)dga.defense.gouv.fr -# - Tanguy Vinceleux (TV) - tanguy.vinceleux(a)dga.defense.gouv.fr -# -# Ce logiciel est régi par la licence CeCILL soumise au droit français et -# respectant les principes de diffusion des logiciels libres. Vous pouvez -# utiliser, modifier et/ou redistribuer ce programme sous les conditions -# de la licence CeCILL telle que diffusée par le CEA, le CNRS et l'INRIA -# sur le site "http://www.cecill.info". Une copie de cette licence est jointe -# dans les fichiers Licence_CeCILL_V2-fr.html et Licence_CeCILL_V2-en.html. -# -# En contrepartie de l'accessibilité au code source et des droits de copie, -# de modification et de redistribution accordés par cette licence, il n'est -# offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, -# seule une responsabilité restreinte pèse sur l'auteur du programme, le -# titulaire des droits patrimoniaux et les concédants successifs. -# -# A cet égard l'attention de l'utilisateur est attirée sur les risques -# associés au chargement, à l'utilisation, à la modification et/ou au -# développement et à la reproduction du logiciel par l'utilisateur étant -# donné sa spécificité de logiciel libre, qui peut le rendre complexe à -# manipuler et qui le réserve donc à des développeurs et des professionnels -# avertis possédant des connaissances informatiques approfondies. Les -# utilisateurs sont donc invités à charger et tester l'adéquation du -# logiciel à leurs besoins dans des conditions permettant d'assurer la -# sécurité de leurs systèmes et ou de leurs données et, plus généralement, -# à l'utiliser et l'exploiter dans les mêmes conditions de sécurité. -# -# Le fait que vous puissiez accéder à cet en-tête signifie que vous avez -# pris connaissance de la licence CeCILL, et que vous en avez accepté les -# termes. - -#------------------------------------------------------------------------------ -# HISTORIQUE: -# 24/10/2004 v0.01 PL: - 1ère version -# 22/12/2004 PL: - séparation dans un module indépendant -# 2004-2007 PL,AK: - nombreuses évolutions -# - contributions de Y. Bidan -# 12/01/2007 v1.00 PL: - version 1.00 officielle -# 2007-09-20 v1.01 PL: - licence CeCILL -# 2007-10-10 PL: - ajout support antivirus ClamAV (clamd) -# 2007-10-22 PL: - ajout support antivirus F-Prot 6 (fpscan) -# 2007-10-28 PL: - ajout Fichier.remplacer_copie_temp() -# 2008-03-24 v1.02 PL: - ajout de _() pour traduction gettext des chaines -# 2010-02-07 v1.03 PL: - removed path module import -# 2010-02-23 v1.04 PL: - updated pyclamd import -# 2010-04-20 v1.05 PL: - added force_extension param to Fichier.__init__ - -#------------------------------------------------------------------------------ -# A FAIRE: -# + antivirus_*: ne pas renvoyer None -# + antivirus_clamd: verif rapport en cas de detection virus -# + antivirus_fpcmd: ajouter codes supérieurs à 8 ? (cf. doc sur site F-Prot) -# + corriger journalisation et debug -# + vérifier que tous les cas d'exceptions sont bien gérés - -# EVOLUTIONS ENVISAGEES: -# - portabilité à améliorer: gérer F-Prot sous Linux/BSD -# => classe générique antivirus ? Voire possibilité de combiner plusieurs -# antivirus ? -# ? séparer nettoyer() en sous-fonctions pour améliorer la lisibilité ? -# ? optimiser lire_debut() pour ne lire qu'une fois le buffer ? (cf. notes) -# ? antivirus: paramètres pour choisir si on scanne systématiquement tous -# les fichiers pour avoir des stats précises du nombre de virus, dans ce cas -# il faudrait le faire avant les filtres de nettoyage (mauvais pour les -# performances), et si on veut scanner tous les types de fichier (option -# "-collect" pour F-Prot) ou bien laisser décider l'antivirus s'il veut -# scanner (option "-type", meilleures perfos mais risque de rater des -# fichiers COM infectés renommés comme eicar.txt => risque acceptable ?) -# ? antivirus: prendre en compte un antivirus local "on-access" qui peut -# empecher l'acces a un fichier (en lecture et/ou en ecriture), au lieu de -# lancer un antivirus en ligne de commande. => Situation typique sous Windows. -#------------------------------------------------------------------------------ - -#=== IMPORTS ================================================================== - -# modules standards Python -import traceback, sys, zipfile, socket, os, os.path -import subprocess - -# modules du projet: -from commun import * -import Resultat, Journal, Rapport, Politique - -# module pyclamd pour utiliser l'antivirus ClamAV (daemon clamd) -try: - import thirdparty.pyclamd.pyclamd as pyclamd -except: - raise ImportError, "missing pyclamd module: "\ - "see http://www.decalage.info/en/python/pyclamd" - - -#=== CONSTANTES =============================================================== - -TAILLE_BUFFER_DEBUT = 4096 # taille du buffer pour l'analyse du début de fichier - -# codes retournés par F-Prot 6 / fpscan (champ de bits): -# (http://www.f-prot.com/support/windows/fpwin_faq/fpscan.html) -# 0: All clean. -# 1: At least one virus-infected object was found and remains. -# 2: At least one suspicious object was found and remains. -# 4: Scanning was aborted by user before it finished; nothing -# found so far. -# 8: Some imposed restrictions were reached causing the scanner -# to skip files (maximum depth of directories, maximum depth -# of archives, exclusion list, etc). -# 16: Some platform error occurred e.g. I/O errors, insufficient -# privileges, out of memory, etc. -# 32: Internal engine error occurred (whatever the engine fails -# at). -# 64: At least one object was not scanned (encrypted file, -# unsupported/unknown compression method, corrupted or invalid -# file). -# 128: At least one object was disinfected. -# NOTE: on considere le resultat "desinfection" comme un virus detecte: -FPSCAN_INFECTION = 1 + 2 + 128 -FPSCAN_ERROR = 4 + 8 + 16 + 32 + 64 - - -#=== CLASSES ================================================================== - -#------------------------------------------------------------------------------ -# classe FICHIER -#------------------- -class Fichier: - """ - classe stockant les informations sur un fichier a analyser. - - Un objet Fichier correspond à un fichier à nettoyer. - - Attributs d'un objet Fichier: - - chemin: objet L{path} correspondant au chemin relatif du fichier dans son conteneur. - - chemin_complet: chemin complet du fichier (incluant le(s) conteneur(s)), objet L{path}. - - nom: chaîne (unicode) correspondant au nom du fichier, sans son chemin. - - extension: chaîne (unicode) correspondant à l'extension du fichier, convertie en minuscules. - - conteneur: conteneur qui contient le fichier. - - resultat_fichier: objet L{Resultat} qui liste les résultats des filtres appliqués sur le fichier. - """ - - def __init__(self, nom_fichier, conteneur, force_extension=None): - """Constructeur de la classe Fichier. - - @param nom_fichier : nom du fichier, chemin relatif par rapport à la racine du conteneur. - @type nom_fichier : str, unicode - - @param conteneur : objet Conteneur qui contient le fichier - @type conteneur : L{Conteneur.Conteneur} - - @param force_extension: if set, force filename extension to a specific value - (used to control which filters are applied) - Note: force_extension may be "" or must start with a dot - @type force_extension: str, unicode - """ - # on vérifie si le nom de fichier fourni est bien un chemin relatif par - # rapport au conteneur: - if chemin_relatif_incorrect(nom_fichier): - debug(_(u"chemin relatif incorrect: %s") % nom_fichier) - raise ValueError, _(u"Nom de fichier non relatif ou incorrect") - self.chemin = path(nom_fichier) - # on construit le chemin absolu à partir de celui du conteneur: - if conteneur.chemin_complet == "": - # le fichier est dans le conteneur racine: chemin direct - self.chemin_complet = self.chemin - else: - self.chemin_complet = conteneur.chemin_complet / self.chemin - self.nom = self.chemin.name - # on extrait l'extension du fichier, convertie en minuscules - # pour permettre une comparaison correcte: - # unless force_extension is used - if force_extension is not None: - # check if it starts with a dot or if it's empty: - assert force_extension.startswith('.') or force_extension=='' - self.extension = force_extension - else: - self.extension = self.chemin.ext.lower() - self.buffer_debut = "" - self.fich_ouvert = None - self._copie_temp = None # chemin vers une copie temporaire du fichier - self.conteneur = conteneur # conteneur du fichier - self.resultat_fichier = Resultat.Resultat(fichier=self) - - def copie_temp (self): - """copie le fichier vers un répertoire temporaire si cela n'a - pas déjà été fait, grâce au conteneur, et retourne le chemin - de la copie.""" - if self._copie_temp != None: - # deja fait - return self._copie_temp - else: - self._copie_temp = self.conteneur.copie_temp(self) - return self._copie_temp - - def rejeter (self): - """lorsqu'un fichier doit être rejeté, supprime la copie - temporaire du fichier si elle existe.""" - try: - if self._copie_temp != None and self._copie_temp.exists(): - self._copie_temp.remove() - except: - #TODO: mieux gerer exception ? - Journal.exception(_(u'Impossible de supprimer un fichier temporaire: %s') % self._copie_temp) - - def remplacer_copie_temp (self, nouveau_fichier): - """ - Pour remplacer la copie temporaire du fichier par une nouvelle - version nettoyee. Les deux fichiers doivent etre sur le meme disque - pour permettre un simple renommage. - Doit etre utilise par tout filtre qui modifie un fichier. - """ - # on modifie la date du nouveau fichier pour correspondre à - # celle d'origine: - date_fich = os.path.getmtime(self._copie_temp) - os.utime(nouveau_fichier, (date_fich, date_fich)) - # on remplace la copie temporaire du fichier d'origine par - # la version nettoyée: - # NOTE: sous Windows on est obligé d'effacer d'abord le fichier - # d'origine, alors que sous Unix il serait simplement écrasé - self._copie_temp.remove() - os.rename(nouveau_fichier, self._copie_temp) - - def lire_debut (self): - """lit le début du fichier et retourne le résultat dans une chaîne. - Le nombre d'octets lus est fixé par la constante TAILLE_BUFFER_DEBUT.""" - # on crée d'abord une copie temporaire du fichier, car celui-ci - # peut être dans un conteneur - self.copie_temp() - # puis on lit le début du fichier dans le buffer. - # NOTE: le mode 'rb' est nécessaire pour lire en mode binaire, - # sinon on lit en mode texte et les fins de lignes peuvent - # être converties, ce qui modifie le fichier. - # NOTE: on relit à chaque appel le début du fichier, car un autre - # filtre peut l'avoir modifié depuis la 1ère lecture. - # On pourrait améliorer un peu les perfos en utilisant un - # flag qui indique si le fichier a été effectivement modifié. - f = file(self._copie_temp, 'rb') - self.buffer_debut = f.read(TAILLE_BUFFER_DEBUT) - f.close() - return self.buffer_debut - - def nettoyer (self, politique): - """Nettoie un fichier, en appelant le(s) filtre(s) correspondant au - format du fichier, puis en effectuant une analyse antivirus. - - @param politique: politique de filtrage à appliquer, objet L{Politique.Politique} - ou None. Si None, la politique par défaut sera appliquée. - @type politique: objet L{Politique.Politique} - - @return: le résultat global des filtres appliqués. - @rtype : objet L{Resultat} - """ - # si une politique est fournie on récupère son dictionnaire de filtres, - # sinon on récupère celle du module Politique: - # (la différence est subtile ;-) - if politique: - dico_filtres = politique.dico_filtres - else: - dico_filtres = Politique.dico_filtres - # on dresse une liste des formats reconnus, pour la journalisation: - formats_reconnus = [] - # si le fichier est chiffré dans son conteneur, on ne peut pas l'analyser: - if self.conteneur.est_chiffre(self): - self.resultat_fichier.ajouter(Resultat.Resultat( - Resultat.REFUSE,[ - _(u"%s : Le fichier est chiffré, il ne peut être analysé.") - % self.conteneur.type], self)) - # sinon on teste si l'extension est reconnue par un des filtres: - elif self.extension in dico_filtres: - # il existe au moins un filtre pour cette extension - # on récupère la liste des filtres concernés - # dans dico_filtres[fichier.extension]: - for filtre in dico_filtres[self.extension]: - format_reconnu = False - # si le format est autorisé, on appelle le filtre: - if filtre.parametres['format_autorise'].valeur == True: - # on appelle d'abord le filtre pour analyser le - # contenu du fichier: - try: - format_reconnu = filtre.reconnait_format(self) - except: - format_reconnu = False - erreur = str(sys.exc_info()[1]) - msg = _(u"%s : Erreur lors de l'analyse du format (%s)") % \ - (filtre.nom, erreur) - resultat_filtre = Resultat.Resultat( - Resultat.ERREUR_ANALYSE, msg, self) - self.resultat_fichier.ajouter(resultat_filtre) - # on ajoute l'exception au journal technique: - Journal.info2(msg, exc_info=True) - # et on fait une pause pour la lire si mode debug: - debug_pause() - if format_reconnu: - # contenu reconnu - Journal.info2(filtre.nom + _(u" : Reconnu")) - # on ajoute ce format à la liste: - formats_reconnus.append(filtre.nom) - # on appelle le filtre pour nettoyer, - # et on récupère le résultat de ce filtre: - try: - resultat_filtre = filtre.nettoyer(self) - except zipfile.BadZipfile: - # si on obtient cette exception, c'est que le module zipfile ne - # supporte pas le format de ce fichier zip. - erreur = str(sys.exc_info()[1]) - #msg = _(u"Le format de l'archive zip est incorrect ou non supporté, ") \ - # + _(u"le fichier ne peut être analysé. (%s)") % erreur - #resultat_filtre = Resultat.Resultat( - # Resultat.ERREUR_ANALYSE, msg, self) - #Journal.info2(msg, exc_info=True) - resultat_filtre = filtre.resultat_format_incorrect(self, erreur) - except: - erreur = str(sys.exc_info()[1]) - msg = _(u"%s : Erreur lors du nettoyage (%s)") % \ - (filtre.nom, erreur) - resultat_filtre = Resultat.Resultat( - Resultat.ERREUR_ANALYSE,msg, self) - # on ajoute l'exception au journal technique: - Journal.info2(msg, exc_info=True) - # et on fait une pause pour la lire si mode debug: - debug_pause() - # on ajoute ce résultat à la liste des - # autres filtres pour ce fichier: - self.resultat_fichier.ajouter(resultat_filtre) - for r in resultat_filtre.raison: - Journal.info2(r) - # Conteneur: c'est le filtre qui doit gérer: -# if filtre.format_conteneur: -# # on crée un objet conteneur -# conteneur = filtre.conteneur(self.chemin, "", self.dico_filtres, self) -# # on nettoie ce conteneur, et on récupère -# # les résultats de chaque fichier (liste) -# liste_resultats = conteneur.nettoyer() -# # on ajoute cette liste au résultat -# self.resultat_fichier.resultats_conteneur = liste_resultats -# liste_resultat.append(liste_resultat2) # à compléter - else: - # sinon le format n'est pas reconnu, on - # met à jour le résultat: - resultat_filtre = Resultat.Resultat( - Resultat.FORMAT_INCORRECT,[filtre.nom \ - + _(u" : Format de fichier non reconnu ou non autorisé")], self) - self.resultat_fichier.ajouter(resultat_filtre) - else: - # le fichier a une extension non autorisée: - # Faut-il ajouter une raison ? - pass - # On appelle l'antivirus après tous les filtres, si le fichier - # n'est pas déjà refusé, pour économiser son analyse qui prend du temps: - # (cela peut poser problème si on veut obtenir des stats précises - # de virus, et d'un autre côté quand un fichier est nettoyé il ne - # devrait plus contenir de code, donc plus de virus... Mais c'est tout - # de même utile pour détecter certains exploits qui se cachent dans des - # données normalement non exécutables) - if not self.resultat_fichier.est_refuse(): - resultat_antivirus = self.antivirus(politique) - # on ajoute le résultat que si qqch de suspect a été détecté: - if resultat_antivirus: - self.resultat_fichier.ajouter(resultat_antivirus) - # tous les filtres sont passés, on regarde le code du résultat final: - # on affiche les détails du résultat: - #print self.resultat_fichier.details() - # on journalise le résultat: - if len(formats_reconnus): - formats = u", ".join(formats_reconnus) + u" -> " - else: - formats = u"" - evt_journal = u"%s : %s%s" % (self.chemin_complet, formats, - self.resultat_fichier.details() ) - if self.resultat_fichier.est_refuse(): - # si c'est un refus, log de niveau WARNING: - Journal.warning(evt_journal) - else: - # sinon, log de niveau INFO: - Journal.info(evt_journal) - # on ajoute le résultat au rapport: - Rapport.ajouter_resultat(self.resultat_fichier) - - - def antivirus (self, politique): - """ - Analyse le fichier grâce à un ou plusieurs antivirus. - - @param politique: politique de filtrage à appliquer, objet L{Politique.Politique} - ou None. Si None, la politique par défaut sera appliquée. - @type politique: objet L{Politique.Politique} - - @return: le résultat de l'analyse, ou None si le fichier est sain. - @rtype : objet L{Resultat} ou None - """ - # on commence avec un resultat vide: - resultat = Resultat.Resultat(fichier=self) - if politique.parametres['antivirus_fpcmd'].valeur: - res = self.antivirus_fpcmd(politique) - if res: resultat.ajouter(res) - if politique.parametres['antivirus_fpscan'].valeur: - res = self.antivirus_fpscan(politique) - if res: resultat.ajouter(res) - if politique.parametres['antivirus_clamd'].valeur: - res = self.antivirus_clamd(politique) - if res: resultat.ajouter(res) - return resultat - - - def antivirus_clamd (self, politique): - """ - Analyse le fichier grâce à l'antivirus ClamAV en mode daemon (clamd). - - @param politique: politique de filtrage à appliquer, objet L{Politique.Politique} - ou None. Si None, la politique par défaut sera appliquée. - @type politique: objet L{Politique.Politique} - - @return: le résultat de l'analyse, ou None si le fichier est sain. - @rtype : objet L{Resultat} ou None - """ - if not pyclamd.use_socket: - # La connexion vers le service clamd n'est pas encore initialisee - serveur = politique.parametres['clamd_serveur'].valeur - port = politique.parametres['clamd_port'].valeur - Journal.info2(_(u'Connexion au serveur clamd %s:%d...') % (serveur, port)) - try: - pyclamd.init_network_socket(serveur, port, timeout=180) - except pyclamd.ScanError: - Journal.exception(_(u"Connexion au service clamd (%s:%d) impossible.") - % (serveur, port)) - raise - Journal.info2(pyclamd.version()) - Journal.info2(_(u"Appel de clamd pour l'analyse antivirus...")) - # clamd a besoin du chemin absolu du fichier: - abspath = str_lat1(self.copie_temp().abspath()) - Journal.debug(_(u'analyse de %s') % abspath) - try: - res = pyclamd.scan_file(abspath) - except socket.timeout: - return Resultat.Resultat(Resultat.ERREUR_ANALYSE, - "clamd: "+_(u"Temps dépassé lors de la vérification antivirus") - % self.nom, self) - except: - msg = "clamd: "+_(u"Erreur lors de la vérification antivirus") - Journal.exception(msg) - return Resultat.Resultat(Resultat.ERREUR_ANALYSE, msg, self) - if res == None: - # resultat OK, pas de virus - Journal.info2("clamd: "+_(u"Pas de virus détecté.")) - # on retourne un objet Resultat vide - return Resultat.Resultat(fichier=self) - else: - # un virus a ete detecte - fichier = res.keys[0] - msg_virus = res[fichier] - Journal.debug("clamd: "+_(u'fichier=%s resultat=%s') %(fichier, msg_virus)) - return Resultat.Resultat(Resultat.REFUSE, - "clamd: "+_(u"Virus détecté ou fichier suspect: %s") % msg_virus, - self) - - - - def antivirus_fpscan (self, politique): - """ - Analyse le fichier grâce à l'antivirus F-Prot 6 (fpscan). - - @param politique: politique de filtrage à appliquer, objet L{Politique.Politique} - ou None. Si None, la politique par défaut sera appliquée. - @type politique: objet L{Politique.Politique} - - @return: le résultat de l'analyse, ou None si le fichier est sain. - @rtype : objet L{Resultat} ou None - """ - if sys.platform == "win32": - # on récupère le chemin de F-Prot: - chemin_fprot = politique.parametres['fpscan_executable'].valeur - Journal.info2(_(u"Lancement de fpscan pour l'analyse antivirus...")) - # Options pour fpscan: - # -t: analyse des alternate data streams sur NTFS (par precaution) - # -v 0: pas d'affichage sauf si erreur ou infection - # /maxdepth=0: pour ne pas analyser l'interieur des archives - # /report: ne tente pas de desinfecter - # En mode debug on affiche tous les messages: - if mode_debug(): verbose = '2' - else: verbose = '0' - chemin_fichier = str_lat1(self.copie_temp()) -## # si le chemin du fichier contient un espace il faut l'entourer de -## # guillemets: => seulement pour os.spawnv, pas pour popen. -## if ' ' in chemin_fichier and not chemin_fichier.startswith('"'): -## chemin_fichier = '"%s"' % chemin_fichier - # Note: avec popen, '-v 0' doit etre separe en 2: '-v', '2' sinon - # F-prot genere une erreur et renvoie un code 20. - args_fprot = [chemin_fprot, '-t', '-v', verbose, '/maxdepth=0', '/report', - chemin_fichier] - Journal.debug(' '.join(args_fprot)) - # On lance F-Prot avec Popen_timer pour masquer son affichage - # et limiter le temps d'exécution: - if mode_debug(): - # on cree un fichier rapport: - args_fprot += ['-o', 'fpscan_debug.txt'] - # en mode debug on ne masque pas l'affichage: -## resultat_fprot = subprocess.call(args_fprot) -## resultat_fprot = os.spawnv(os.P_WAIT, chemin_fprot, args_fprot[1:]) - resultat_fprot = Popen_timer(args_fprot, stdout=sys.stdout, - stderr=sys.stderr) - else: - resultat_fprot = Popen_timer(args_fprot) - Journal.debug(_(u'resultat F-Prot fpscan: %d') % resultat_fprot) - if resultat_fprot == 0: - Journal.info2(u"fpscan: "+_(u"Pas de virus détecté.")) - resultat = None - # on teste en premier s'il y a eu un timeout: - elif resultat_fprot == EXIT_KILL_PTIMER: - resultat = Resultat.Resultat(Resultat.ERREUR_ANALYSE, - u"fpscan: "+_(u"Temps dépassé lors de la vérification antivirus"), - self) - elif resultat_fprot & FPSCAN_INFECTION: - resultat = Resultat.Resultat(Resultat.REFUSE, - u"fpscan: "+_(u"Virus détecté ou fichier suspect"), - self) - else: - resultat = Resultat.Resultat(Resultat.ERREUR_ANALYSE, - u"fpscan: "+_(u"Erreur lors de la vérification antivirus"), - self) - else: - # pour l'instant seul F-Prot sous Windows est supporté: - raise NotImplementedError, \ - u"fpscan: "+_(u"Antivirus non supporte ou implemente pour ce systeme.") - return resultat - - - def antivirus_fpcmd (self, politique): - """ - Analyse le fichier grâce à l'antivirus F-Prot 3 (fpcmd). - - @param politique: politique de filtrage à appliquer, objet L{Politique.Politique} - ou None. Si None, la politique par défaut sera appliquée. - @type politique: objet L{Politique.Politique} - - @return: le résultat de l'analyse, ou None si le fichier est sain. - @rtype : objet L{Resultat} ou None - """ - if sys.platform == "win32": - # on récupère le chemin de fpcmd: - chemin_fprot = politique.parametres['fpcmd_executable'].valeur - Journal.info2(_(u"Lancement de fpcmd pour l'analyse antivirus...")) - args_fprot = [chemin_fprot, '-nomem', '-noboot', '-nosub', '-silent', - '-old', '-collect', str_lat1(self.copie_temp())] - # A VOIR: l'option -type laisse F-Prot décider si un type de fichier doit - # être scanné, alors que -collect force F-Prot à tout scanner - # (risque de faux-positifs) - - # On lance F-Prot avec Popen_timer pour masquer son affichage - # et limiter le temps d'exécution: - resultat_fprot = Popen_timer(args_fprot) - # codes retournés par F-Prot 3: - # 0 - Normal exit - nothing found - # 1 - Abnormal termination - unrecoverable error. This can mean any of - # the following: - # Internal error in the program. - # DOS version prior to 3.0 was used. - # ENGLISH.TX0, SIGN.DEF or MACRO.DEF corrupted or not present. - # 2 - Selftest failed - program has been modified. - # 3 - A Boot/File virus infection found. - # 4 - Virus found in memory. - # 5 - Program terminated with ^C or ESC. - # 6 - A virus was removed. This code is only meaningful if - # the program is used to scan just a single file. - # 7 - Insufficient memory to run the program. - # 8 - At least one suspicious file was found, but no infections. - if resultat_fprot == 0: - Journal.info2(u"fpcmd: "+_(u"Pas de virus détecté.")) - resultat = None - elif resultat_fprot in [3, 4, 6, 8]: - resultat = Resultat.Resultat(Resultat.REFUSE, - u"fpcmd: "+_(u"Virus détecté ou fichier suspect"), - self) - elif resultat_fprot == EXIT_KILL_PTIMER: - resultat = Resultat.Resultat(Resultat.ERREUR_ANALYSE, - u"fpcmd: "+_(u"Temps dépassé lors de la vérification antivirus"), - self) - else: - resultat = Resultat.Resultat(Resultat.ERREUR_ANALYSE, - u"fpcmd: "+_(u"Erreur lors de la vérification antivirus"), - self) - else: - # pour l'instant seul F-Prot sous Windows est supporté: - raise NotImplementedError, \ - u"fpcmd: "+_(u"Antivirus non supporte ou implemente pour ce systeme.") +#!/usr/bin/python +# -*- coding: iso-8859-1 -*- +""" +Fichier - ExeFilter + +Module qui contient la classe L{Fichier.Fichier}, pour representer un fichier a +analyser. + +Ce fichier fait partie du projet ExeFilter. +URL du projet: U{http://www.decalage.info/exefilter} + +@organization: DGA/CELAR +@author: U{Philippe Lagadec} +@author: U{Arnaud Kerréneur} + +@contact: U{Philippe Lagadec} + +@copyright: DGA/CELAR 2004-2008 +@copyright: NATO/NC3A 2008-2010 (PL changes after ExeFilter v1.1.0) +@license: CeCILL (open-source compatible GPL) - cf. code source ou fichier LICENCE.txt joint + +@version: 1.05 + +@status: beta +""" + +#============================================================================== +__docformat__ = 'epytext en' + +__date__ = "2010-04-20" +__version__ = "1.05" + +#------------------------------------------------------------------------------ +# LICENCE pour le projet ExeFilter: + +# Copyright DGA/CELAR 2004-2008 +# Copyright NATO/NC3A 2008-2010 (PL changes after ExeFilter v1.1.0) +# Auteurs: +# - Philippe Lagadec (PL) - philippe.lagadec(a)laposte.net +# - Arnaud Kerréneur (AK) - arnaud.kerreneur(a)dga.defense.gouv.fr +# - Tanguy Vinceleux (TV) - tanguy.vinceleux(a)dga.defense.gouv.fr +# +# Ce logiciel est régi par la licence CeCILL soumise au droit français et +# respectant les principes de diffusion des logiciels libres. Vous pouvez +# utiliser, modifier et/ou redistribuer ce programme sous les conditions +# de la licence CeCILL telle que diffusée par le CEA, le CNRS et l'INRIA +# sur le site "http://www.cecill.info". Une copie de cette licence est jointe +# dans les fichiers Licence_CeCILL_V2-fr.html et Licence_CeCILL_V2-en.html. +# +# En contrepartie de l'accessibilité au code source et des droits de copie, +# de modification et de redistribution accordés par cette licence, il n'est +# offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, +# seule une responsabilité restreinte pèse sur l'auteur du programme, le +# titulaire des droits patrimoniaux et les concédants successifs. +# +# A cet égard l'attention de l'utilisateur est attirée sur les risques +# associés au chargement, à l'utilisation, à la modification et/ou au +# développement et à la reproduction du logiciel par l'utilisateur étant +# donné sa spécificité de logiciel libre, qui peut le rendre complexe à +# manipuler et qui le réserve donc à des développeurs et des professionnels +# avertis possédant des connaissances informatiques approfondies. Les +# utilisateurs sont donc invités à charger et tester l'adéquation du +# logiciel à leurs besoins dans des conditions permettant d'assurer la +# sécurité de leurs systèmes et ou de leurs données et, plus généralement, +# à l'utiliser et l'exploiter dans les mêmes conditions de sécurité. +# +# Le fait que vous puissiez accéder à cet en-tête signifie que vous avez +# pris connaissance de la licence CeCILL, et que vous en avez accepté les +# termes. + +#------------------------------------------------------------------------------ +# HISTORIQUE: +# 24/10/2004 v0.01 PL: - 1ère version +# 22/12/2004 PL: - séparation dans un module indépendant +# 2004-2007 PL,AK: - nombreuses évolutions +# - contributions de Y. Bidan +# 12/01/2007 v1.00 PL: - version 1.00 officielle +# 2007-09-20 v1.01 PL: - licence CeCILL +# 2007-10-10 PL: - ajout support antivirus ClamAV (clamd) +# 2007-10-22 PL: - ajout support antivirus F-Prot 6 (fpscan) +# 2007-10-28 PL: - ajout Fichier.remplacer_copie_temp() +# 2008-03-24 v1.02 PL: - ajout de _() pour traduction gettext des chaines +# 2010-02-07 v1.03 PL: - removed path module import +# 2010-02-23 v1.04 PL: - updated pyclamd import +# 2010-04-20 v1.05 PL: - added force_extension param to Fichier.__init__ + +#------------------------------------------------------------------------------ +# A FAIRE: +# + antivirus_*: ne pas renvoyer None +# + antivirus_clamd: verif rapport en cas de detection virus +# + antivirus_fpcmd: ajouter codes supérieurs à 8 ? (cf. doc sur site F-Prot) +# + corriger journalisation et debug +# + vérifier que tous les cas d'exceptions sont bien gérés + +# EVOLUTIONS ENVISAGEES: +# - portabilité à améliorer: gérer F-Prot sous Linux/BSD +# => classe générique antivirus ? Voire possibilité de combiner plusieurs +# antivirus ? +# ? séparer nettoyer() en sous-fonctions pour améliorer la lisibilité ? +# ? optimiser lire_debut() pour ne lire qu'une fois le buffer ? (cf. notes) +# ? antivirus: paramètres pour choisir si on scanne systématiquement tous +# les fichiers pour avoir des stats précises du nombre de virus, dans ce cas +# il faudrait le faire avant les filtres de nettoyage (mauvais pour les +# performances), et si on veut scanner tous les types de fichier (option +# "-collect" pour F-Prot) ou bien laisser décider l'antivirus s'il veut +# scanner (option "-type", meilleures perfos mais risque de rater des +# fichiers COM infectés renommés comme eicar.txt => risque acceptable ?) +# ? antivirus: prendre en compte un antivirus local "on-access" qui peut +# empecher l'acces a un fichier (en lecture et/ou en ecriture), au lieu de +# lancer un antivirus en ligne de commande. => Situation typique sous Windows. +#------------------------------------------------------------------------------ + +#=== IMPORTS ================================================================== + +# modules standards Python +import traceback, sys, zipfile, socket, os, os.path +import subprocess + +# modules du projet: +from commun import * +import Resultat, Journal, Rapport, Politique + +# module pyclamd pour utiliser l'antivirus ClamAV (daemon clamd) +try: + import thirdparty.pyclamd.pyclamd as pyclamd +except: + raise ImportError("missing pyclamd module: " + "see http://www.decalage.info/en/python/pyclamd") + + +#=== CONSTANTES =============================================================== + +TAILLE_BUFFER_DEBUT = 4096 # taille du buffer pour l'analyse du début de fichier + +# codes retournés par F-Prot 6 / fpscan (champ de bits): +# (http://www.f-prot.com/support/windows/fpwin_faq/fpscan.html) +# 0: All clean. +# 1: At least one virus-infected object was found and remains. +# 2: At least one suspicious object was found and remains. +# 4: Scanning was aborted by user before it finished; nothing +# found so far. +# 8: Some imposed restrictions were reached causing the scanner +# to skip files (maximum depth of directories, maximum depth +# of archives, exclusion list, etc). +# 16: Some platform error occurred e.g. I/O errors, insufficient +# privileges, out of memory, etc. +# 32: Internal engine error occurred (whatever the engine fails +# at). +# 64: At least one object was not scanned (encrypted file, +# unsupported/unknown compression method, corrupted or invalid +# file). +# 128: At least one object was disinfected. +# NOTE: on considere le resultat "desinfection" comme un virus detecte: +FPSCAN_INFECTION = 1 + 2 + 128 +FPSCAN_ERROR = 4 + 8 + 16 + 32 + 64 + + +#=== CLASSES ================================================================== + +#------------------------------------------------------------------------------ +# classe FICHIER +#------------------- +class Fichier: + """ + classe stockant les informations sur un fichier a analyser. + + Un objet Fichier correspond à un fichier à nettoyer. + + Attributs d'un objet Fichier: + - chemin: objet L{path} correspondant au chemin relatif du fichier dans son conteneur. + - chemin_complet: chemin complet du fichier (incluant le(s) conteneur(s)), objet L{path}. + - nom: chaîne (unicode) correspondant au nom du fichier, sans son chemin. + - extension: chaîne (unicode) correspondant à l'extension du fichier, convertie en minuscules. + - conteneur: conteneur qui contient le fichier. + - resultat_fichier: objet L{Resultat} qui liste les résultats des filtres appliqués sur le fichier. + """ + + def __init__(self, nom_fichier, conteneur, force_extension=None): + """Constructeur de la classe Fichier. + + @param nom_fichier : nom du fichier, chemin relatif par rapport à la racine du conteneur. + @type nom_fichier : str, unicode + + @param conteneur : objet Conteneur qui contient le fichier + @type conteneur : L{Conteneur.Conteneur} + + @param force_extension: if set, force filename extension to a specific value + (used to control which filters are applied) + Note: force_extension may be "" or must start with a dot + @type force_extension: str, unicode + """ + # on vérifie si le nom de fichier fourni est bien un chemin relatif par + # rapport au conteneur: + if chemin_relatif_incorrect(nom_fichier): + debug(_(u"chemin relatif incorrect: %s") % nom_fichier) + raise ValueError(_(u"Nom de fichier non relatif ou incorrect")) + self.chemin = path(nom_fichier) + # on construit le chemin absolu à partir de celui du conteneur: + if conteneur.chemin_complet == "": + # le fichier est dans le conteneur racine: chemin direct + self.chemin_complet = self.chemin + else: + self.chemin_complet = conteneur.chemin_complet / self.chemin + self.nom = self.chemin.name + # on extrait l'extension du fichier, convertie en minuscules + # pour permettre une comparaison correcte: + # unless force_extension is used + if force_extension is not None: + # check if it starts with a dot or if it's empty: + assert force_extension.startswith('.') or force_extension=='' + self.extension = force_extension + else: + self.extension = self.chemin.ext.lower() + self.buffer_debut = "" + self.fich_ouvert = None + self._copie_temp = None # chemin vers une copie temporaire du fichier + self.conteneur = conteneur # conteneur du fichier + self.resultat_fichier = Resultat.Resultat(fichier=self) + + def copie_temp (self): + """copie le fichier vers un répertoire temporaire si cela n'a + pas déjà été fait, grâce au conteneur, et retourne le chemin + de la copie.""" + if self._copie_temp != None: + # deja fait + return self._copie_temp + else: + self._copie_temp = self.conteneur.copie_temp(self) + return self._copie_temp + + def rejeter (self): + """lorsqu'un fichier doit être rejeté, supprime la copie + temporaire du fichier si elle existe.""" + try: + if self._copie_temp != None and self._copie_temp.exists(): + self._copie_temp.remove() + except: + #TODO: mieux gerer exception ? + Journal.exception(_(u'Impossible de supprimer un fichier temporaire: %s') % self._copie_temp) + + def remplacer_copie_temp (self, nouveau_fichier): + """ + Pour remplacer la copie temporaire du fichier par une nouvelle + version nettoyee. Les deux fichiers doivent etre sur le meme disque + pour permettre un simple renommage. + Doit etre utilise par tout filtre qui modifie un fichier. + """ + # on modifie la date du nouveau fichier pour correspondre à + # celle d'origine: + date_fich = os.path.getmtime(self._copie_temp) + os.utime(nouveau_fichier, (date_fich, date_fich)) + # on remplace la copie temporaire du fichier d'origine par + # la version nettoyée: + # NOTE: sous Windows on est obligé d'effacer d'abord le fichier + # d'origine, alors que sous Unix il serait simplement écrasé + self._copie_temp.remove() + os.rename(nouveau_fichier, self._copie_temp) + + def lire_debut (self): + """lit le début du fichier et retourne le résultat dans une chaîne. + Le nombre d'octets lus est fixé par la constante TAILLE_BUFFER_DEBUT.""" + # on crée d'abord une copie temporaire du fichier, car celui-ci + # peut être dans un conteneur + self.copie_temp() + # puis on lit le début du fichier dans le buffer. + # NOTE: le mode 'rb' est nécessaire pour lire en mode binaire, + # sinon on lit en mode texte et les fins de lignes peuvent + # être converties, ce qui modifie le fichier. + # NOTE: on relit à chaque appel le début du fichier, car un autre + # filtre peut l'avoir modifié depuis la 1ère lecture. + # On pourrait améliorer un peu les perfos en utilisant un + # flag qui indique si le fichier a été effectivement modifié. + f = open(self._copie_temp, 'rb') + self.buffer_debut = f.read(TAILLE_BUFFER_DEBUT) + f.close() + return self.buffer_debut + + def nettoyer (self, politique): + """Nettoie un fichier, en appelant le(s) filtre(s) correspondant au + format du fichier, puis en effectuant une analyse antivirus. + + @param politique: politique de filtrage à appliquer, objet L{Politique.Politique} + ou None. Si None, la politique par défaut sera appliquée. + @type politique: objet L{Politique.Politique} + + @return: le résultat global des filtres appliqués. + @rtype : objet L{Resultat} + """ + # si une politique est fournie on récupère son dictionnaire de filtres, + # sinon on récupère celle du module Politique: + # (la différence est subtile ;-) + if politique: + dico_filtres = politique.dico_filtres + else: + dico_filtres = Politique.dico_filtres + # on dresse une liste des formats reconnus, pour la journalisation: + formats_reconnus = [] + # si le fichier est chiffré dans son conteneur, on ne peut pas l'analyser: + if self.conteneur.est_chiffre(self): + self.resultat_fichier.ajouter(Resultat.Resultat( + Resultat.REFUSE,[ + _(u"%s : Le fichier est chiffré, il ne peut être analysé.") + % self.conteneur.type], self)) + # sinon on teste si l'extension est reconnue par un des filtres: + elif self.extension in dico_filtres: + # il existe au moins un filtre pour cette extension + # on récupère la liste des filtres concernés + # dans dico_filtres[fichier.extension]: + for filtre in dico_filtres[self.extension]: + format_reconnu = False + # si le format est autorisé, on appelle le filtre: + if filtre.parametres['format_autorise'].valeur == True: + # on appelle d'abord le filtre pour analyser le + # contenu du fichier: + try: + format_reconnu = filtre.reconnait_format(self) + except: + format_reconnu = False + erreur = str(sys.exc_info()[1]) + msg = _(u"%s : Erreur lors de l'analyse du format (%s)") % \ + (filtre.nom, erreur) + resultat_filtre = Resultat.Resultat( + Resultat.ERREUR_ANALYSE, msg, self) + self.resultat_fichier.ajouter(resultat_filtre) + # on ajoute l'exception au journal technique: + Journal.info2(msg, exc_info=True) + # et on fait une pause pour la lire si mode debug: + debug_pause() + if format_reconnu: + # contenu reconnu + Journal.info2(filtre.nom + _(u" : Reconnu")) + # on ajoute ce format à la liste: + formats_reconnus.append(filtre.nom) + # on appelle le filtre pour nettoyer, + # et on récupère le résultat de ce filtre: + try: + resultat_filtre = filtre.nettoyer(self) + except zipfile.BadZipfile: + # si on obtient cette exception, c'est que le module zipfile ne + # supporte pas le format de ce fichier zip. + erreur = str(sys.exc_info()[1]) + #msg = _(u"Le format de l'archive zip est incorrect ou non supporté, ") \ + # + _(u"le fichier ne peut être analysé. (%s)") % erreur + #resultat_filtre = Resultat.Resultat( + # Resultat.ERREUR_ANALYSE, msg, self) + #Journal.info2(msg, exc_info=True) + resultat_filtre = filtre.resultat_format_incorrect(self, erreur) + except: + erreur = str(sys.exc_info()[1]) + msg = _(u"%s : Erreur lors du nettoyage (%s)") % \ + (filtre.nom, erreur) + resultat_filtre = Resultat.Resultat( + Resultat.ERREUR_ANALYSE,msg, self) + # on ajoute l'exception au journal technique: + Journal.info2(msg, exc_info=True) + # et on fait une pause pour la lire si mode debug: + debug_pause() + # on ajoute ce résultat à la liste des + # autres filtres pour ce fichier: + self.resultat_fichier.ajouter(resultat_filtre) + for r in resultat_filtre.raison: + Journal.info2(r) + # Conteneur: c'est le filtre qui doit gérer: +# if filtre.format_conteneur: +# # on crée un objet conteneur +# conteneur = filtre.conteneur(self.chemin, "", self.dico_filtres, self) +# # on nettoie ce conteneur, et on récupère +# # les résultats de chaque fichier (liste) +# liste_resultats = conteneur.nettoyer() +# # on ajoute cette liste au résultat +# self.resultat_fichier.resultats_conteneur = liste_resultats +# liste_resultat.append(liste_resultat2) # à compléter + else: + # sinon le format n'est pas reconnu, on + # met à jour le résultat: + resultat_filtre = Resultat.Resultat( + Resultat.FORMAT_INCORRECT,[filtre.nom \ + + _(u" : Format de fichier non reconnu ou non autorisé")], self) + self.resultat_fichier.ajouter(resultat_filtre) + else: + # le fichier a une extension non autorisée: + # Faut-il ajouter une raison ? + pass + # On appelle l'antivirus après tous les filtres, si le fichier + # n'est pas déjà refusé, pour économiser son analyse qui prend du temps: + # (cela peut poser problème si on veut obtenir des stats précises + # de virus, et d'un autre côté quand un fichier est nettoyé il ne + # devrait plus contenir de code, donc plus de virus... Mais c'est tout + # de même utile pour détecter certains exploits qui se cachent dans des + # données normalement non exécutables) + if not self.resultat_fichier.est_refuse(): + resultat_antivirus = self.antivirus(politique) + # on ajoute le résultat que si qqch de suspect a été détecté: + if resultat_antivirus: + self.resultat_fichier.ajouter(resultat_antivirus) + # tous les filtres sont passés, on regarde le code du résultat final: + # on affiche les détails du résultat: + #print self.resultat_fichier.details() + # on journalise le résultat: + if len(formats_reconnus): + formats = u", ".join(formats_reconnus) + u" -> " + else: + formats = u"" + evt_journal = u"%s : %s%s" % (self.chemin_complet, formats, + self.resultat_fichier.details() ) + if self.resultat_fichier.est_refuse(): + # si c'est un refus, log de niveau WARNING: + Journal.warning(evt_journal) + else: + # sinon, log de niveau INFO: + Journal.info(evt_journal) + # on ajoute le résultat au rapport: + Rapport.ajouter_resultat(self.resultat_fichier) + + + def antivirus (self, politique): + """ + Analyse le fichier grâce à un ou plusieurs antivirus. + + @param politique: politique de filtrage à appliquer, objet L{Politique.Politique} + ou None. Si None, la politique par défaut sera appliquée. + @type politique: objet L{Politique.Politique} + + @return: le résultat de l'analyse, ou None si le fichier est sain. + @rtype : objet L{Resultat} ou None + """ + # on commence avec un resultat vide: + resultat = Resultat.Resultat(fichier=self) + if politique.parametres['antivirus_fpcmd'].valeur: + res = self.antivirus_fpcmd(politique) + if res: resultat.ajouter(res) + if politique.parametres['antivirus_fpscan'].valeur: + res = self.antivirus_fpscan(politique) + if res: resultat.ajouter(res) + if politique.parametres['antivirus_clamd'].valeur: + res = self.antivirus_clamd(politique) + if res: resultat.ajouter(res) + return resultat + + + def antivirus_clamd (self, politique): + """ + Analyse le fichier grâce à l'antivirus ClamAV en mode daemon (clamd). + + @param politique: politique de filtrage à appliquer, objet L{Politique.Politique} + ou None. Si None, la politique par défaut sera appliquée. + @type politique: objet L{Politique.Politique} + + @return: le résultat de l'analyse, ou None si le fichier est sain. + @rtype : objet L{Resultat} ou None + """ + if not pyclamd.use_socket: + # La connexion vers le service clamd n'est pas encore initialisee + serveur = politique.parametres['clamd_serveur'].valeur + port = politique.parametres['clamd_port'].valeur + Journal.info2(_(u'Connexion au serveur clamd %s:%d...') % (serveur, port)) + try: + pyclamd.init_network_socket(serveur, port, timeout=180) + except pyclamd.ScanError: + Journal.exception(_(u"Connexion au service clamd (%s:%d) impossible.") + % (serveur, port)) + raise + Journal.info2(pyclamd.version()) + Journal.info2(_(u"Appel de clamd pour l'analyse antivirus...")) + # clamd a besoin du chemin absolu du fichier: + abspath = str_lat1(self.copie_temp().abspath()) + Journal.debug(_(u'analyse de %s') % abspath) + try: + res = pyclamd.scan_file(abspath) + except socket.timeout: + return Resultat.Resultat(Resultat.ERREUR_ANALYSE, + "clamd: "+_(u"Temps dépassé lors de la vérification antivirus") + % self.nom, self) + except: + msg = "clamd: "+_(u"Erreur lors de la vérification antivirus") + Journal.exception(msg) + return Resultat.Resultat(Resultat.ERREUR_ANALYSE, msg, self) + if res == None: + # resultat OK, pas de virus + Journal.info2("clamd: "+_(u"Pas de virus détecté.")) + # on retourne un objet Resultat vide + return Resultat.Resultat(fichier=self) + else: + # un virus a ete detecte + fichier = res.keys[0] + msg_virus = res[fichier] + Journal.debug("clamd: "+_(u'fichier=%s resultat=%s') %(fichier, msg_virus)) + return Resultat.Resultat(Resultat.REFUSE, + "clamd: "+_(u"Virus détecté ou fichier suspect: %s") % msg_virus, + self) + + + + def antivirus_fpscan (self, politique): + """ + Analyse le fichier grâce à l'antivirus F-Prot 6 (fpscan). + + @param politique: politique de filtrage à appliquer, objet L{Politique.Politique} + ou None. Si None, la politique par défaut sera appliquée. + @type politique: objet L{Politique.Politique} + + @return: le résultat de l'analyse, ou None si le fichier est sain. + @rtype : objet L{Resultat} ou None + """ + if sys.platform == "win32": + # on récupère le chemin de F-Prot: + chemin_fprot = politique.parametres['fpscan_executable'].valeur + Journal.info2(_(u"Lancement de fpscan pour l'analyse antivirus...")) + # Options pour fpscan: + # -t: analyse des alternate data streams sur NTFS (par precaution) + # -v 0: pas d'affichage sauf si erreur ou infection + # /maxdepth=0: pour ne pas analyser l'interieur des archives + # /report: ne tente pas de desinfecter + # En mode debug on affiche tous les messages: + if mode_debug(): verbose = '2' + else: verbose = '0' + chemin_fichier = str_lat1(self.copie_temp()) +## # si le chemin du fichier contient un espace il faut l'entourer de +## # guillemets: => seulement pour os.spawnv, pas pour popen. +## if ' ' in chemin_fichier and not chemin_fichier.startswith('"'): +## chemin_fichier = '"%s"' % chemin_fichier + # Note: avec popen, '-v 0' doit etre separe en 2: '-v', '2' sinon + # F-prot genere une erreur et renvoie un code 20. + args_fprot = [chemin_fprot, '-t', '-v', verbose, '/maxdepth=0', '/report', + chemin_fichier] + Journal.debug(' '.join(args_fprot)) + # On lance F-Prot avec Popen_timer pour masquer son affichage + # et limiter le temps d'exécution: + if mode_debug(): + # on cree un fichier rapport: + args_fprot += ['-o', 'fpscan_debug.txt'] + # en mode debug on ne masque pas l'affichage: +## resultat_fprot = subprocess.call(args_fprot) +## resultat_fprot = os.spawnv(os.P_WAIT, chemin_fprot, args_fprot[1:]) + resultat_fprot = Popen_timer(args_fprot, stdout=sys.stdout, + stderr=sys.stderr) + else: + resultat_fprot = Popen_timer(args_fprot) + Journal.debug(_(u'resultat F-Prot fpscan: %d') % resultat_fprot) + if resultat_fprot == 0: + Journal.info2(u"fpscan: "+_(u"Pas de virus détecté.")) + resultat = None + # on teste en premier s'il y a eu un timeout: + elif resultat_fprot == EXIT_KILL_PTIMER: + resultat = Resultat.Resultat(Resultat.ERREUR_ANALYSE, + u"fpscan: "+_(u"Temps dépassé lors de la vérification antivirus"), + self) + elif resultat_fprot & FPSCAN_INFECTION: + resultat = Resultat.Resultat(Resultat.REFUSE, + u"fpscan: "+_(u"Virus détecté ou fichier suspect"), + self) + else: + resultat = Resultat.Resultat(Resultat.ERREUR_ANALYSE, + u"fpscan: "+_(u"Erreur lors de la vérification antivirus"), + self) + else: + # pour l'instant seul F-Prot sous Windows est supporté: + raise NotImplementedError( u"fpscan: "+_(u"Antivirus non supporte ou implemente pour ce systeme.")) + return resultat + + + def antivirus_fpcmd (self, politique): + """ + Analyse le fichier grâce à l'antivirus F-Prot 3 (fpcmd). + + @param politique: politique de filtrage à appliquer, objet L{Politique.Politique} + ou None. Si None, la politique par défaut sera appliquée. + @type politique: objet L{Politique.Politique} + + @return: le résultat de l'analyse, ou None si le fichier est sain. + @rtype : objet L{Resultat} ou None + """ + if sys.platform == "win32": + # on récupère le chemin de fpcmd: + chemin_fprot = politique.parametres['fpcmd_executable'].valeur + Journal.info2(_(u"Lancement de fpcmd pour l'analyse antivirus...")) + args_fprot = [chemin_fprot, '-nomem', '-noboot', '-nosub', '-silent', + '-old', '-collect', str_lat1(self.copie_temp())] + # A VOIR: l'option -type laisse F-Prot décider si un type de fichier doit + # être scanné, alors que -collect force F-Prot à tout scanner + # (risque de faux-positifs) + + # On lance F-Prot avec Popen_timer pour masquer son affichage + # et limiter le temps d'exécution: + resultat_fprot = Popen_timer(args_fprot) + # codes retournés par F-Prot 3: + # 0 - Normal exit - nothing found + # 1 - Abnormal termination - unrecoverable error. This can mean any of + # the following: + # Internal error in the program. + # DOS version prior to 3.0 was used. + # ENGLISH.TX0, SIGN.DEF or MACRO.DEF corrupted or not present. + # 2 - Selftest failed - program has been modified. + # 3 - A Boot/File virus infection found. + # 4 - Virus found in memory. + # 5 - Program terminated with ^C or ESC. + # 6 - A virus was removed. This code is only meaningful if + # the program is used to scan just a single file. + # 7 - Insufficient memory to run the program. + # 8 - At least one suspicious file was found, but no infections. + if resultat_fprot == 0: + Journal.info2(u"fpcmd: "+_(u"Pas de virus détecté.")) + resultat = None + elif resultat_fprot in [3, 4, 6, 8]: + resultat = Resultat.Resultat(Resultat.REFUSE, + u"fpcmd: "+_(u"Virus détecté ou fichier suspect"), + self) + elif resultat_fprot == EXIT_KILL_PTIMER: + resultat = Resultat.Resultat(Resultat.ERREUR_ANALYSE, + u"fpcmd: "+_(u"Temps dépassé lors de la vérification antivirus"), + self) + else: + resultat = Resultat.Resultat(Resultat.ERREUR_ANALYSE, + u"fpcmd: "+_(u"Erreur lors de la vérification antivirus"), + self) + else: + # pour l'instant seul F-Prot sous Windows est supporté: + raise NotImplementedError( u"fpcmd: "+_(u"Antivirus non supporte ou implemente pour ce systeme.")) return resultat \ No newline at end of file diff --git a/Filtres/Filtre_AVI.py b/Filtres/Filtre_AVI.py index c6c9840..e74994d 100644 --- a/Filtres/Filtre_AVI.py +++ b/Filtres/Filtre_AVI.py @@ -1,144 +1,144 @@ -#!/usr/bin/python -# -*- coding: iso-8859-1 -*- -""" -Filtre_AVI - ExeFilter - -Ce module contient la classe L{Filtre_AVI.Filtre_AVI}, -pour filtrer les fichiers vidéo AVI. - -Ce fichier fait partie du projet ExeFilter. -URL du projet: U{http://www.decalage.info/exefilter} - -@organization: DGA/CELAR -@author: U{Philippe Lagadec} -@author: U{Arnaud Kerréneur} -@author: U{Tanguy Vinceleux} - -@contact: U{Philippe Lagadec} - -@copyright: DGA/CELAR 2004-2008 -@copyright: NATO/NC3A 2008 (modifications PL apres v1.1.0) - -@license: CeCILL (open-source compatible GPL) - cf. code source ou fichier LICENCE.txt joint - -@version: 1.02 - -@status: beta -""" -#============================================================================== -__docformat__ = 'epytext en' - -__date__ = "2008-03-24" -__version__ = "1.02" - -#------------------------------------------------------------------------------ -# LICENCE pour le projet ExeFilter: - -# Copyright DGA/CELAR 2004-2008 -# Copyright NATO/NC3A 2008 (PL changes after v1.1.0) -# Auteurs: -# - Philippe Lagadec (PL) - philippe.lagadec(a)laposte.net -# - Arnaud Kerréneur (AK) - arnaud.kerreneur(a)dga.defense.gouv.fr -# - Tanguy Vinceleux (TV) - tanguy.vinceleux(a)dga.defense.gouv.fr -# -# Ce logiciel est régi par la licence CeCILL soumise au droit français et -# respectant les principes de diffusion des logiciels libres. Vous pouvez -# utiliser, modifier et/ou redistribuer ce programme sous les conditions -# de la licence CeCILL telle que diffusée par le CEA, le CNRS et l'INRIA -# sur le site "http://www.cecill.info". Une copie de cette licence est jointe -# dans les fichiers Licence_CeCILL_V2-fr.html et Licence_CeCILL_V2-en.html. -# -# En contrepartie de l'accessibilité au code source et des droits de copie, -# de modification et de redistribution accordés par cette licence, il n'est -# offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, -# seule une responsabilité restreinte pèse sur l'auteur du programme, le -# titulaire des droits patrimoniaux et les concédants successifs. -# -# A cet égard l'attention de l'utilisateur est attirée sur les risques -# associés au chargement, à l'utilisation, à la modification et/ou au -# développement et à la reproduction du logiciel par l'utilisateur étant -# donné sa spécificité de logiciel libre, qui peut le rendre complexe à -# manipuler et qui le réserve donc à des développeurs et des professionnels -# avertis possédant des connaissances informatiques approfondies. Les -# utilisateurs sont donc invités à charger et tester l'adéquation du -# logiciel à leurs besoins dans des conditions permettant d'assurer la -# sécurité de leurs systèmes et ou de leurs données et, plus généralement, -# à l'utiliser et l'exploiter dans les mêmes conditions de sécurité. -# -# Le fait que vous puissiez accéder à cet en-tête signifie que vous avez -# pris connaissance de la licence CeCILL, et que vous en avez accepté les -# termes. - -#------------------------------------------------------------------------------ -# HISTORIQUE: -# 22/12/2004 v0.01 TV: - 1ère version -# 2004-2005 TV,PL: - evolutions -# 12/01/2007 v1.00 PL: - version 1.00 officielle -# 2007-09-20 v1.01 PL: - licence CeCILL -# 2008-03-24 v1.02 PL: - ajout de _() pour traduction gettext des chaines -# - simplification dans nettoyer() en appelant resultat_* - -# A FAIRE: -# + Tenir compte des octets indiquant la taille du fichier -#------------------------------------------------------------------------------ - -#=== IMPORTS ================================================================== - -# modules du projet: -from commun import * -import Filtre, Resultat - -#=== CONSTANTES =============================================================== - - -#=== CLASSES ================================================================== - -#------------------------------------------------------------------------------ -# classe FILTRE_AVI -#------------------- -class Filtre_AVI (Filtre.Filtre): - """ - classe pour un filtre de fichiers AVI. - - un objet Filtre sert à reconnaître le format d'un fichier et à - nettoyer le code éventuel qu'il contient. La classe Filtre_AVI - correspond aux fichiers video ".avi". - - @cvar nom: Le nom detaillé du filtre - @cvar nom_code: nom de code du filtre - @cvar extensions: liste des extensions de fichiers possibles - @cvar format_conteneur: indique si c'est un format conteneur - @cvar extractible: indique si il s'agit d'une archive - @cvar nettoyable: indique si il est possible de nettoyer avec ce filtre - @cvar date: date de la derniere modification du filtre - @cvar version: version du filtre - - """ - - nom = _(u"Fichier Video AVI") - extensions = [".avi"] - format_conteneur = False - extractible = False - nettoyable = True - - # date et version définies à partir de celles du module - date = __date__ - version = __version__ - - def reconnait_format(self, fichier): - """analyse le format du fichier, et retourne True s'il correspond - au format recherché, False sinon.""" - - debut = fichier.lire_debut() - if debut.startswith("\x52\x49\x46\x46") and debut[8:11]==("\x41\x56\x49"): - return True - - def nettoyer (self, fichier): - """analyse et modifie le fichier pour supprimer tout code - exécutable qu'il peut contenir, si cela est possible. - Retourne un code résultat suivant l'action effectuée.""" - # Ce format ne contient jamais de code. - # (sauf bien sûr d'éventuels "exploits" qui sont normalement détectés - # par l'antivirus) - return self.resultat_accepte(fichier) +#!/usr/bin/python +# -*- coding: iso-8859-1 -*- +""" +Filtre_AVI - ExeFilter + +Ce module contient la classe L{Filtre_AVI.Filtre_AVI}, +pour filtrer les fichiers vidéo AVI. + +Ce fichier fait partie du projet ExeFilter. +URL du projet: U{http://www.decalage.info/exefilter} + +@organization: DGA/CELAR +@author: U{Philippe Lagadec} +@author: U{Arnaud Kerréneur} +@author: U{Tanguy Vinceleux} + +@contact: U{Philippe Lagadec} + +@copyright: DGA/CELAR 2004-2008 +@copyright: NATO/NC3A 2008 (modifications PL apres v1.1.0) + +@license: CeCILL (open-source compatible GPL) + cf. code source ou fichier LICENCE.txt joint + +@version: 1.02 + +@status: beta +""" +#============================================================================== +__docformat__ = 'epytext en' + +__date__ = "2008-03-24" +__version__ = "1.02" + +#------------------------------------------------------------------------------ +# LICENCE pour le projet ExeFilter: + +# Copyright DGA/CELAR 2004-2008 +# Copyright NATO/NC3A 2008 (PL changes after v1.1.0) +# Auteurs: +# - Philippe Lagadec (PL) - philippe.lagadec(a)laposte.net +# - Arnaud Kerréneur (AK) - arnaud.kerreneur(a)dga.defense.gouv.fr +# - Tanguy Vinceleux (TV) - tanguy.vinceleux(a)dga.defense.gouv.fr +# +# Ce logiciel est régi par la licence CeCILL soumise au droit français et +# respectant les principes de diffusion des logiciels libres. Vous pouvez +# utiliser, modifier et/ou redistribuer ce programme sous les conditions +# de la licence CeCILL telle que diffusée par le CEA, le CNRS et l'INRIA +# sur le site "http://www.cecill.info". Une copie de cette licence est jointe +# dans les fichiers Licence_CeCILL_V2-fr.html et Licence_CeCILL_V2-en.html. +# +# En contrepartie de l'accessibilité au code source et des droits de copie, +# de modification et de redistribution accordés par cette licence, il n'est +# offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, +# seule une responsabilité restreinte pèse sur l'auteur du programme, le +# titulaire des droits patrimoniaux et les concédants successifs. +# +# A cet égard l'attention de l'utilisateur est attirée sur les risques +# associés au chargement, à l'utilisation, à la modification et/ou au +# développement et à la reproduction du logiciel par l'utilisateur étant +# donné sa spécificité de logiciel libre, qui peut le rendre complexe à +# manipuler et qui le réserve donc à des développeurs et des professionnels +# avertis possédant des connaissances informatiques approfondies. Les +# utilisateurs sont donc invités à charger et tester l'adéquation du +# logiciel à leurs besoins dans des conditions permettant d'assurer la +# sécurité de leurs systèmes et ou de leurs données et, plus généralement, +# à l'utiliser et l'exploiter dans les mêmes conditions de sécurité. +# +# Le fait que vous puissiez accéder à cet en-tête signifie que vous avez +# pris connaissance de la licence CeCILL, et que vous en avez accepté les +# termes. + +#------------------------------------------------------------------------------ +# HISTORIQUE: +# 22/12/2004 v0.01 TV: - 1ère version +# 2004-2005 TV,PL: - evolutions +# 12/01/2007 v1.00 PL: - version 1.00 officielle +# 2007-09-20 v1.01 PL: - licence CeCILL +# 2008-03-24 v1.02 PL: - ajout de _() pour traduction gettext des chaines +# - simplification dans nettoyer() en appelant resultat_* + +# A FAIRE: +# + Tenir compte des octets indiquant la taille du fichier +#------------------------------------------------------------------------------ + +#=== IMPORTS ================================================================== + +# modules du projet: +from commun import * +import Resultat +from . import Filtre +#=== CONSTANTES =============================================================== + + +#=== CLASSES ================================================================== + +#------------------------------------------------------------------------------ +# classe FILTRE_AVI +#------------------- +class Filtre_AVI (Filtre.Filtre): + """ + classe pour un filtre de fichiers AVI. + + un objet Filtre sert à reconnaître le format d'un fichier et à + nettoyer le code éventuel qu'il contient. La classe Filtre_AVI + correspond aux fichiers video ".avi". + + @cvar nom: Le nom detaillé du filtre + @cvar nom_code: nom de code du filtre + @cvar extensions: liste des extensions de fichiers possibles + @cvar format_conteneur: indique si c'est un format conteneur + @cvar extractible: indique si il s'agit d'une archive + @cvar nettoyable: indique si il est possible de nettoyer avec ce filtre + @cvar date: date de la derniere modification du filtre + @cvar version: version du filtre + + """ + + nom = _(u"Fichier Video AVI") + extensions = [".avi"] + format_conteneur = False + extractible = False + nettoyable = True + + # date et version définies à partir de celles du module + date = __date__ + version = __version__ + + def reconnait_format(self, fichier): + """analyse le format du fichier, et retourne True s'il correspond + au format recherché, False sinon.""" + + debut = fichier.lire_debut() + if debut.startswith(b"\x52\x49\x46\x46") and debut[8:11]==(b"\x41\x56\x49"): + return True + + def nettoyer (self, fichier): + """analyse et modifie le fichier pour supprimer tout code + exécutable qu'il peut contenir, si cela est possible. + Retourne un code résultat suivant l'action effectuée.""" + # Ce format ne contient jamais de code. + # (sauf bien sûr d'éventuels "exploits" qui sont normalement détectés + # par l'antivirus) + return self.resultat_accepte(fichier) diff --git a/Filtres/Filtre_BMP.py b/Filtres/Filtre_BMP.py index 15931a3..066b459 100644 --- a/Filtres/Filtre_BMP.py +++ b/Filtres/Filtre_BMP.py @@ -1,143 +1,143 @@ -#!/usr/bin/python -# -*- coding: iso-8859-1 -*- -""" -Filtre_BMP - ExeFilter - -Ce module contient la classe L{Filtre_BMP.Filtre_BMP}, -pour filtrer les fichiers images BMP. - -Ce fichier fait partie du projet ExeFilter. -URL du projet: U{http://www.decalage.info/exefilter} - -@organization: DGA/CELAR -@author: U{Philippe Lagadec} -@author: U{Arnaud Kerréneur} -@author: U{Tanguy Vinceleux} - -@contact: U{Philippe Lagadec} - -@copyright: DGA/CELAR 2004-2008 -@copyright: NATO/NC3A 2008 (modifications PL apres v1.1.0) - -@license: CeCILL (open-source compatible GPL) - cf. code source ou fichier LICENCE.txt joint - -@version: 1.02 - -@status: beta -""" -#============================================================================== -__docformat__ = 'epytext en' - -__date__ = "2008-03-24" -__version__ = "1.02" - -#------------------------------------------------------------------------------ -# LICENCE pour le projet ExeFilter: - -# Copyright DGA/CELAR 2004-2008 -# Copyright NATO/NC3A 2008 (PL changes after v1.1.0) -# Auteurs: -# - Philippe Lagadec (PL) - philippe.lagadec(a)laposte.net -# - Arnaud Kerréneur (AK) - arnaud.kerreneur(a)dga.defense.gouv.fr -# - Tanguy Vinceleux (TV) - tanguy.vinceleux(a)dga.defense.gouv.fr -# -# Ce logiciel est régi par la licence CeCILL soumise au droit français et -# respectant les principes de diffusion des logiciels libres. Vous pouvez -# utiliser, modifier et/ou redistribuer ce programme sous les conditions -# de la licence CeCILL telle que diffusée par le CEA, le CNRS et l'INRIA -# sur le site "http://www.cecill.info". Une copie de cette licence est jointe -# dans les fichiers Licence_CeCILL_V2-fr.html et Licence_CeCILL_V2-en.html. -# -# En contrepartie de l'accessibilité au code source et des droits de copie, -# de modification et de redistribution accordés par cette licence, il n'est -# offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, -# seule une responsabilité restreinte pèse sur l'auteur du programme, le -# titulaire des droits patrimoniaux et les concédants successifs. -# -# A cet égard l'attention de l'utilisateur est attirée sur les risques -# associés au chargement, à l'utilisation, à la modification et/ou au -# développement et à la reproduction du logiciel par l'utilisateur étant -# donné sa spécificité de logiciel libre, qui peut le rendre complexe à -# manipuler et qui le réserve donc à des développeurs et des professionnels -# avertis possédant des connaissances informatiques approfondies. Les -# utilisateurs sont donc invités à charger et tester l'adéquation du -# logiciel à leurs besoins dans des conditions permettant d'assurer la -# sécurité de leurs systèmes et ou de leurs données et, plus généralement, -# à l'utiliser et l'exploiter dans les mêmes conditions de sécurité. -# -# Le fait que vous puissiez accéder à cet en-tête signifie que vous avez -# pris connaissance de la licence CeCILL, et que vous en avez accepté les -# termes. - -#------------------------------------------------------------------------------ -# HISTORIQUE: -# 22/12/2004 v0.01 TV: - 1ère version -# 2004-2005 TV,PL: - evolutions -# 12/01/2007 v1.00 PL: - version 1.00 officielle -# 2007-09-20 v1.01 PL: - licence CeCILL -# 2008-03-24 v1.02 PL: - ajout de _() pour traduction gettext des chaines -# - simplification dans nettoyer() en appelant resultat_* - -# A FAIRE: -# + tenir compte des octets indiquant la taille du fichier -#------------------------------------------------------------------------------ - -#=== IMPORTS ================================================================== - -# modules du projet: -from commun import * -import Filtre, Resultat - -#=== CONSTANTES =============================================================== - - -#=== CLASSES ================================================================== - -#------------------------------------------------------------------------------ -# classe FILTRE_TEXTE -#------------------- -class Filtre_BMP (Filtre.Filtre): - """ - classe pour un filtre de fichiers BMP. - - un objet Filtre sert à reconnaître le format d'un fichier et à - nettoyer le code éventuel qu'il contient. La classe Filtre_Texte - correspond aux fichiers texte ASCII 8 bits. - - @cvar nom: Le nom detaillé du filtre - @cvar nom_code: nom de code du filtre - @cvar extensions: liste des extensions de fichiers possibles - @cvar format_conteneur: indique si c'est un format conteneur - @cvar extractible: indique si il s'agit d'une archive - @cvar nettoyable: indique si il est possible de nettoyer avec ce filtre - @cvar date: date de la derniere modification du filtre - @cvar version: version du filtre - - """ - - nom = _(u"Fichier Image BMP") - extensions = [".bmp",".dib"] - format_conteneur = False - extractible = False - nettoyable = False - - # date et version définies à partir de celles du module - date = __date__ - version = __version__ - - def reconnait_format(self, fichier): - """analyse le format du fichier, et retourne True s'il correspond - au format recherché, False sinon.""" - debut = fichier.lire_debut() - if debut.startswith("\x42\x4D"): - return True - - def nettoyer (self, fichier): - """analyse et modifie le fichier pour supprimer tout code - exécutable qu'il peut contenir, si cela est possible. - Retourne un code résultat suivant l'action effectuée.""" - # Ce format ne contient jamais de code. - # (sauf bien sûr d'éventuels "exploits" qui sont normalement détectés - # par l'antivirus) - return self.resultat_accepte(fichier) +#!/usr/bin/python +# -*- coding: iso-8859-1 -*- +""" +Filtre_BMP - ExeFilter + +Ce module contient la classe L{Filtre_BMP.Filtre_BMP}, +pour filtrer les fichiers images BMP. + +Ce fichier fait partie du projet ExeFilter. +URL du projet: U{http://www.decalage.info/exefilter} + +@organization: DGA/CELAR +@author: U{Philippe Lagadec} +@author: U{Arnaud Kerréneur} +@author: U{Tanguy Vinceleux} + +@contact: U{Philippe Lagadec} + +@copyright: DGA/CELAR 2004-2008 +@copyright: NATO/NC3A 2008 (modifications PL apres v1.1.0) + +@license: CeCILL (open-source compatible GPL) + cf. code source ou fichier LICENCE.txt joint + +@version: 1.02 + +@status: beta +""" +#============================================================================== +__docformat__ = 'epytext en' + +__date__ = "2008-03-24" +__version__ = "1.02" + +#------------------------------------------------------------------------------ +# LICENCE pour le projet ExeFilter: + +# Copyright DGA/CELAR 2004-2008 +# Copyright NATO/NC3A 2008 (PL changes after v1.1.0) +# Auteurs: +# - Philippe Lagadec (PL) - philippe.lagadec(a)laposte.net +# - Arnaud Kerréneur (AK) - arnaud.kerreneur(a)dga.defense.gouv.fr +# - Tanguy Vinceleux (TV) - tanguy.vinceleux(a)dga.defense.gouv.fr +# +# Ce logiciel est régi par la licence CeCILL soumise au droit français et +# respectant les principes de diffusion des logiciels libres. Vous pouvez +# utiliser, modifier et/ou redistribuer ce programme sous les conditions +# de la licence CeCILL telle que diffusée par le CEA, le CNRS et l'INRIA +# sur le site "http://www.cecill.info". Une copie de cette licence est jointe +# dans les fichiers Licence_CeCILL_V2-fr.html et Licence_CeCILL_V2-en.html. +# +# En contrepartie de l'accessibilité au code source et des droits de copie, +# de modification et de redistribution accordés par cette licence, il n'est +# offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, +# seule une responsabilité restreinte pèse sur l'auteur du programme, le +# titulaire des droits patrimoniaux et les concédants successifs. +# +# A cet égard l'attention de l'utilisateur est attirée sur les risques +# associés au chargement, à l'utilisation, à la modification et/ou au +# développement et à la reproduction du logiciel par l'utilisateur étant +# donné sa spécificité de logiciel libre, qui peut le rendre complexe à +# manipuler et qui le réserve donc à des développeurs et des professionnels +# avertis possédant des connaissances informatiques approfondies. Les +# utilisateurs sont donc invités à charger et tester l'adéquation du +# logiciel à leurs besoins dans des conditions permettant d'assurer la +# sécurité de leurs systèmes et ou de leurs données et, plus généralement, +# à l'utiliser et l'exploiter dans les mêmes conditions de sécurité. +# +# Le fait que vous puissiez accéder à cet en-tête signifie que vous avez +# pris connaissance de la licence CeCILL, et que vous en avez accepté les +# termes. + +#------------------------------------------------------------------------------ +# HISTORIQUE: +# 22/12/2004 v0.01 TV: - 1ère version +# 2004-2005 TV,PL: - evolutions +# 12/01/2007 v1.00 PL: - version 1.00 officielle +# 2007-09-20 v1.01 PL: - licence CeCILL +# 2008-03-24 v1.02 PL: - ajout de _() pour traduction gettext des chaines +# - simplification dans nettoyer() en appelant resultat_* + +# A FAIRE: +# + tenir compte des octets indiquant la taille du fichier +#------------------------------------------------------------------------------ + +#=== IMPORTS ================================================================== + +# modules du projet: +from commun import * +import Resultat +from . import Filtre +#=== CONSTANTES =============================================================== + + +#=== CLASSES ================================================================== + +#------------------------------------------------------------------------------ +# classe FILTRE_TEXTE +#------------------- +class Filtre_BMP (Filtre.Filtre): + """ + classe pour un filtre de fichiers BMP. + + un objet Filtre sert à reconnaître le format d'un fichier et à + nettoyer le code éventuel qu'il contient. La classe Filtre_Texte + correspond aux fichiers texte ASCII 8 bits. + + @cvar nom: Le nom detaillé du filtre + @cvar nom_code: nom de code du filtre + @cvar extensions: liste des extensions de fichiers possibles + @cvar format_conteneur: indique si c'est un format conteneur + @cvar extractible: indique si il s'agit d'une archive + @cvar nettoyable: indique si il est possible de nettoyer avec ce filtre + @cvar date: date de la derniere modification du filtre + @cvar version: version du filtre + + """ + + nom = _(u"Fichier Image BMP") + extensions = [".bmp",".dib"] + format_conteneur = False + extractible = False + nettoyable = False + + # date et version définies à partir de celles du module + date = __date__ + version = __version__ + + def reconnait_format(self, fichier): + """analyse le format du fichier, et retourne True s'il correspond + au format recherché, False sinon.""" + debut = fichier.lire_debut() + if debut.startswith(b"\x42\x4D"): + return True + + def nettoyer (self, fichier): + """analyse et modifie le fichier pour supprimer tout code + exécutable qu'il peut contenir, si cela est possible. + Retourne un code résultat suivant l'action effectuée.""" + # Ce format ne contient jamais de code. + # (sauf bien sûr d'éventuels "exploits" qui sont normalement détectés + # par l'antivirus) + return self.resultat_accepte(fichier) diff --git a/Filtres/Filtre_GIF.py b/Filtres/Filtre_GIF.py index f17a860..f14c944 100644 --- a/Filtres/Filtre_GIF.py +++ b/Filtres/Filtre_GIF.py @@ -1,141 +1,141 @@ -#!/usr/bin/python -# -*- coding: iso-8859-1 -*- -""" -Filtre_GIF - ExeFilter - -Ce module contient la classe L{Filtre_GIF.Filtre_GIF}, -pour filtrer les fichiers images GIF. - -Ce fichier fait partie du projet ExeFilter. -URL du projet: U{http://www.decalage.info/exefilter} - -@organization: DGA/CELAR -@author: U{Philippe Lagadec} -@author: U{Arnaud Kerréneur} -@author: U{Tanguy Vinceleux} - -@contact: U{Philippe Lagadec} - -@copyright: DGA/CELAR 2004-2008 -@copyright: NATO/NC3A 2008 (modifications PL apres v1.1.0) - -@license: CeCILL (open-source compatible GPL) - cf. code source ou fichier LICENCE.txt joint - -@version: 1.02 - -@status: beta -""" -#============================================================================== -__docformat__ = 'epytext en' - -__date__ = "2008-03-24" -__version__ = "1.02" - -#------------------------------------------------------------------------------ -# LICENCE pour le projet ExeFilter: - -# Copyright DGA/CELAR 2004-2008 -# Copyright NATO/NC3A 2008 (PL changes after v1.1.0) -# Auteurs: -# - Philippe Lagadec (PL) - philippe.lagadec(a)laposte.net -# - Arnaud Kerréneur (AK) - arnaud.kerreneur(a)dga.defense.gouv.fr -# - Tanguy Vinceleux (TV) - tanguy.vinceleux(a)dga.defense.gouv.fr -# -# Ce logiciel est régi par la licence CeCILL soumise au droit français et -# respectant les principes de diffusion des logiciels libres. Vous pouvez -# utiliser, modifier et/ou redistribuer ce programme sous les conditions -# de la licence CeCILL telle que diffusée par le CEA, le CNRS et l'INRIA -# sur le site "http://www.cecill.info". Une copie de cette licence est jointe -# dans les fichiers Licence_CeCILL_V2-fr.html et Licence_CeCILL_V2-en.html. -# -# En contrepartie de l'accessibilité au code source et des droits de copie, -# de modification et de redistribution accordés par cette licence, il n'est -# offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, -# seule une responsabilité restreinte pèse sur l'auteur du programme, le -# titulaire des droits patrimoniaux et les concédants successifs. -# -# A cet égard l'attention de l'utilisateur est attirée sur les risques -# associés au chargement, à l'utilisation, à la modification et/ou au -# développement et à la reproduction du logiciel par l'utilisateur étant -# donné sa spécificité de logiciel libre, qui peut le rendre complexe à -# manipuler et qui le réserve donc à des développeurs et des professionnels -# avertis possédant des connaissances informatiques approfondies. Les -# utilisateurs sont donc invités à charger et tester l'adéquation du -# logiciel à leurs besoins dans des conditions permettant d'assurer la -# sécurité de leurs systèmes et ou de leurs données et, plus généralement, -# à l'utiliser et l'exploiter dans les mêmes conditions de sécurité. -# -# Le fait que vous puissiez accéder à cet en-tête signifie que vous avez -# pris connaissance de la licence CeCILL, et que vous en avez accepté les -# termes. - -#------------------------------------------------------------------------------ -# HISTORIQUE: -# 22/12/2004 v0.01 TV: - 1ère version -# 2004-2005 TV,PL: - evolutions -# 12/01/2007 v1.00 PL: - version 1.00 officielle -# 2007-09-20 v1.01 PL: - licence CeCILL -# 2008-03-24 v1.02 PL: - ajout de _() pour traduction gettext des chaines -# - simplification dans nettoyer() en appelant resultat_* - -# A FAIRE: -#------------------------------------------------------------------------------ - -#=== IMPORTS ================================================================== - -# modules du projet: -from commun import * -import Filtre, Resultat - -#=== CONSTANTES =============================================================== - - -#=== CLASSES ================================================================== - -#------------------------------------------------------------------------------ -# classe FILTRE_GIF -#------------------- -class Filtre_GIF (Filtre.Filtre): - """ - classe pour un filtre de fichiers Gif. - - un objet Filtre sert à reconnaître le format d'un fichier et à - nettoyer le code éventuel qu'il contient. La classe Filtre_GIF - correspond aux fichiers images GIF. - - @cvar nom: Le nom detaillé du filtre - @cvar nom_code: nom de code du filtre - @cvar extensions: liste des extensions de fichiers possibles - @cvar format_conteneur: indique si c'est un format conteneur - @cvar extractible: indique si il s'agit d'une archive - @cvar nettoyable: indique si il est possible de nettoyer avec ce filtre - @cvar date: date de la derniere modification du filtre - @cvar version: version du filtre - """ - - nom = _(u"Fichier Image GIF") - extensions = [".gif"] - format_conteneur = False - extractible = False - nettoyable = False - - # date et version définies à partir de celles du module - date = __date__ - version = __version__ - - def reconnait_format(self, fichier): - """analyse le format du fichier, et retourne True s'il correspond - au format recherché, False sinon.""" - debut = fichier.lire_debut() - if debut.startswith("\x47\x49\x46\x38"): - return True - - def nettoyer (self, fichier): - """analyse et modifie le fichier pour supprimer tout code - exécutable qu'il peut contenir, si cela est possible. - Retourne un code résultat suivant l'action effectuée.""" - # Ce format ne contient jamais de code. - # (sauf bien sûr d'éventuels "exploits" qui sont normalement détectés - # par l'antivirus) - return self.resultat_accepte(fichier) +#!/usr/bin/python +# -*- coding: iso-8859-1 -*- +""" +Filtre_GIF - ExeFilter + +Ce module contient la classe L{Filtre_GIF.Filtre_GIF}, +pour filtrer les fichiers images GIF. + +Ce fichier fait partie du projet ExeFilter. +URL du projet: U{http://www.decalage.info/exefilter} + +@organization: DGA/CELAR +@author: U{Philippe Lagadec} +@author: U{Arnaud Kerréneur} +@author: U{Tanguy Vinceleux} + +@contact: U{Philippe Lagadec} + +@copyright: DGA/CELAR 2004-2008 +@copyright: NATO/NC3A 2008 (modifications PL apres v1.1.0) + +@license: CeCILL (open-source compatible GPL) + cf. code source ou fichier LICENCE.txt joint + +@version: 1.02 + +@status: beta +""" +#============================================================================== +__docformat__ = 'epytext en' + +__date__ = "2008-03-24" +__version__ = "1.02" + +#------------------------------------------------------------------------------ +# LICENCE pour le projet ExeFilter: + +# Copyright DGA/CELAR 2004-2008 +# Copyright NATO/NC3A 2008 (PL changes after v1.1.0) +# Auteurs: +# - Philippe Lagadec (PL) - philippe.lagadec(a)laposte.net +# - Arnaud Kerréneur (AK) - arnaud.kerreneur(a)dga.defense.gouv.fr +# - Tanguy Vinceleux (TV) - tanguy.vinceleux(a)dga.defense.gouv.fr +# +# Ce logiciel est régi par la licence CeCILL soumise au droit français et +# respectant les principes de diffusion des logiciels libres. Vous pouvez +# utiliser, modifier et/ou redistribuer ce programme sous les conditions +# de la licence CeCILL telle que diffusée par le CEA, le CNRS et l'INRIA +# sur le site "http://www.cecill.info". Une copie de cette licence est jointe +# dans les fichiers Licence_CeCILL_V2-fr.html et Licence_CeCILL_V2-en.html. +# +# En contrepartie de l'accessibilité au code source et des droits de copie, +# de modification et de redistribution accordés par cette licence, il n'est +# offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, +# seule une responsabilité restreinte pèse sur l'auteur du programme, le +# titulaire des droits patrimoniaux et les concédants successifs. +# +# A cet égard l'attention de l'utilisateur est attirée sur les risques +# associés au chargement, à l'utilisation, à la modification et/ou au +# développement et à la reproduction du logiciel par l'utilisateur étant +# donné sa spécificité de logiciel libre, qui peut le rendre complexe à +# manipuler et qui le réserve donc à des développeurs et des professionnels +# avertis possédant des connaissances informatiques approfondies. Les +# utilisateurs sont donc invités à charger et tester l'adéquation du +# logiciel à leurs besoins dans des conditions permettant d'assurer la +# sécurité de leurs systèmes et ou de leurs données et, plus généralement, +# à l'utiliser et l'exploiter dans les mêmes conditions de sécurité. +# +# Le fait que vous puissiez accéder à cet en-tête signifie que vous avez +# pris connaissance de la licence CeCILL, et que vous en avez accepté les +# termes. + +#------------------------------------------------------------------------------ +# HISTORIQUE: +# 22/12/2004 v0.01 TV: - 1ère version +# 2004-2005 TV,PL: - evolutions +# 12/01/2007 v1.00 PL: - version 1.00 officielle +# 2007-09-20 v1.01 PL: - licence CeCILL +# 2008-03-24 v1.02 PL: - ajout de _() pour traduction gettext des chaines +# - simplification dans nettoyer() en appelant resultat_* + +# A FAIRE: +#------------------------------------------------------------------------------ + +#=== IMPORTS ================================================================== + +# modules du projet: +from commun import * +import Resultat +from . import Filtre +#=== CONSTANTES =============================================================== + + +#=== CLASSES ================================================================== + +#------------------------------------------------------------------------------ +# classe FILTRE_GIF +#------------------- +class Filtre_GIF (Filtre.Filtre): + """ + classe pour un filtre de fichiers Gif. + + un objet Filtre sert à reconnaître le format d'un fichier et à + nettoyer le code éventuel qu'il contient. La classe Filtre_GIF + correspond aux fichiers images GIF. + + @cvar nom: Le nom detaillé du filtre + @cvar nom_code: nom de code du filtre + @cvar extensions: liste des extensions de fichiers possibles + @cvar format_conteneur: indique si c'est un format conteneur + @cvar extractible: indique si il s'agit d'une archive + @cvar nettoyable: indique si il est possible de nettoyer avec ce filtre + @cvar date: date de la derniere modification du filtre + @cvar version: version du filtre + """ + + nom = _(u"Fichier Image GIF") + extensions = [".gif"] + format_conteneur = False + extractible = False + nettoyable = False + + # date et version définies à partir de celles du module + date = __date__ + version = __version__ + + def reconnait_format(self, fichier): + """analyse le format du fichier, et retourne True s'il correspond + au format recherché, False sinon.""" + debut = fichier.lire_debut() + if debut.startswith(b"\x47\x49\x46\x38"): + return True + + def nettoyer (self, fichier): + """analyse et modifie le fichier pour supprimer tout code + exécutable qu'il peut contenir, si cela est possible. + Retourne un code résultat suivant l'action effectuée.""" + # Ce format ne contient jamais de code. + # (sauf bien sûr d'éventuels "exploits" qui sont normalement détectés + # par l'antivirus) + return self.resultat_accepte(fichier) diff --git a/Filtres/Filtre_HTML.py b/Filtres/Filtre_HTML.py index 7b601dd..1c49f52 100644 --- a/Filtres/Filtre_HTML.py +++ b/Filtres/Filtre_HTML.py @@ -1,592 +1,599 @@ -#!/usr/bin/python -# -*- coding: iso-8859-1 -*- -""" -Filtre_HTML - ExeFilter - -Ce module contient la classe L{Filtre_HTML.Filtre_HTML} permettant de filtrer -les fichiers de type "document HTML". - -Ce fichier fait partie du projet ExeFilter. -URL du projet: U{http://www.decalage.info/exefilter} - -@organization: DGA/CELAR -@author: U{Philippe Lagadec} -@author: U{Arnaud Kerréneur} -@author: U{Tanguy Vinceleux} - -@contact: U{Philippe Lagadec} - -@copyright: DGA/CELAR 2004-2008 -@copyright: NATO/NC3A 2008-2010 (modifications PL apres ExeFilter v1.1.0) - -@license: CeCILL (open-source compatible GPL) - cf. code source ou fichier LICENCE.txt joint - -@version: 1.04 - -@status: beta -""" -#============================================================================== -__docformat__ = 'epytext en' - -__date__ = "2011-02-18" -__version__ = "1.04" - -#------------------------------------------------------------------------------ -# LICENCE pour le projet ExeFilter: - -# Copyright DGA/CELAR 2004-2008 -# Copyright NATO/NC3A 2008-2010 (PL changes after ExeFilter v1.1.0) -# Auteurs: -# - Philippe Lagadec (PL) - philippe.lagadec(a)laposte.net -# - Arnaud Kerréneur (AK) - arnaud.kerreneur(a)dga.defense.gouv.fr -# - Tanguy Vinceleux (TV) - tanguy.vinceleux(a)dga.defense.gouv.fr -# -# Ce logiciel est régi par la licence CeCILL soumise au droit français et -# respectant les principes de diffusion des logiciels libres. Vous pouvez -# utiliser, modifier et/ou redistribuer ce programme sous les conditions -# de la licence CeCILL telle que diffusée par le CEA, le CNRS et l'INRIA -# sur le site "http://www.cecill.info". Une copie de cette licence est jointe -# dans les fichiers Licence_CeCILL_V2-fr.html et Licence_CeCILL_V2-en.html. -# -# En contrepartie de l'accessibilité au code source et des droits de copie, -# de modification et de redistribution accordés par cette licence, il n'est -# offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, -# seule une responsabilité restreinte pèse sur l'auteur du programme, le -# titulaire des droits patrimoniaux et les concédants successifs. -# -# A cet égard l'attention de l'utilisateur est attirée sur les risques -# associés au chargement, à l'utilisation, à la modification et/ou au -# développement et à la reproduction du logiciel par l'utilisateur étant -# donné sa spécificité de logiciel libre, qui peut le rendre complexe à -# manipuler et qui le réserve donc à des développeurs et des professionnels -# avertis possédant des connaissances informatiques approfondies. Les -# utilisateurs sont donc invités à charger et tester l'adéquation du -# logiciel à leurs besoins dans des conditions permettant d'assurer la -# sécurité de leurs systèmes et ou de leurs données et, plus généralement, -# à l'utiliser et l'exploiter dans les mêmes conditions de sécurité. -# -# Le fait que vous puissiez accéder à cet en-tête signifie que vous avez -# pris connaissance de la licence CeCILL, et que vous en avez accepté les -# termes. - -#------------------------------------------------------------------------------ -# HISTORIQUE: -# 29/10/2004 v0.01 PL: - 1ère version -# 2004-2006 PL,AK: - evolutions -# 12/01/2007 v1.00 PL: - version 1.00 officielle -# 2008-02-24 v1.01 PL: - licence CeCILL -# 2008-03-24 v1.02 PL: - ajout de _() pour traduction gettext des chaines -# - simplification dans nettoyer() en appelant resultat_* -# 2010-02-23 v1.03 PL: - updated HTMLParser_PL import -# 2011-02-18 v1.04 PL: - fixed temp file creation using new commun functions - -#------------------------------------------------------------------------------ -# TODO: -# + option to use lxml.html or html5lib instead of HTMLParser, which is too -# picky for many normal but slightly malformed HTML pages. See: -# http://lxml.de/lxmlhtml.html -# http://lxml.de/lxmlhtml.html#cleaning-up-html -# => seems great but need to add code to report what was cleaned -# http://lxml.de/elementsoup.html -# http://lxml.de/elementsoup.html#using-soupparser-as-a-fallback -# http://lxml.de/html5parser.html -# http://wiki.whatwg.org/wiki/Sanitization_rules -# http://code.google.com/p/html5lib/wiki/UserDocumentation#Sanitizing_Tokenizer -# BUT the issue is how to be informed when html is actually sanitized... -#------------------------------------------------------------------------------ - -#=== IMPORTS ================================================================== - -# modules standards Python: -import os, sys, urlparse, tempfile, codecs, traceback, HTMLParser - -# modules du projet: -import commun -from commun import * -import Resultat, Conteneur -import Filtre -import thirdparty.HTMLParser_PL.HTMLParser_PL as HTMLParser_PL - - -#=== CONSTANTES =============================================================== - -# liste des protocoles autorisés dans une URL: -PROTOCOLES_OK = ["file", "http", "https", "ftp", "mailto"] - -# liste des balises HTML à supprimer: -BALISES_SUPPR = ['script', 'object', 'embed', 'applet', 'xml', 'iframe'] - -# Byte-Order Markers utilisés comme entêtes pour les encodages Unicode ou UTF-8: -BOM = { - 'utf_16_be': codecs.BOM_UTF16_BE, - 'utf_16_le': codecs.BOM_UTF16_LE, - 'utf_8' : codecs.BOM_UTF8 - } - -# nombre d'octets lus à chaque lecture du fichier -TAILLE_BUFFER = 65536 - -#=== CLASSES ================================================================== - -# Classe de base pour les parseurs HTML: -# (permet de changer facilement cette classe sans toucher au code) -HTMLParser_base = HTMLParser_PL.HTMLParser_PL -#HTMLParser_base = HTMLParser.HTMLParser - -#============================================================================== -# classe HTML_META -#===================== -class HTML_META (HTMLParser_base): - """Pour analyser un code HTML et déterminer si une balise META indique un - encodage différent de celui du BOM. - - Après analyse complète du code HTML, le résultat peut être lu dans - l'attribut encodage_META (None ou nom du codec trouvé). - Si une incohérence est détectée (encodages META et BOM différents, ou bien - deux balises META différentes), une exception ValueError est levée. - - @ivar encodage_META: encodage spécifié par une balise META. - """ - - def __init__(self, encodage_BOM=None): - """constructeur pour un objet HTML_META. - - @param encodage_BOM: nom du codec déduit du BOM, None sinon. - @type encodage_BOM: str, None - """ - # on initialise d'abord avec le constructeur de la classe de base: - HTMLParser_base.__init__(self) - # encodage déduit du BOM - self.encodage_BOM = encodage_BOM - # encodage spécifié par balise META - self.encodage_META = None - - def handle_starttag(self, tag, attrs): - """Pour traiter une balise HTML ouvrante. - Seules les balises META sont prises en compte. - """ - # on ne prend en compte que les balises META: - if tag != 'meta': return - # flag pour noter si on a un http-equiv=content-type - http_equiv = False - # charset vide au départ - charset = "" - for attr in attrs: - # conversion du nom et de la valeur de l'attribut en minuscule: - nom_attr = attr[0].lower() - val_attr = attr[1].lower() - if nom_attr == "content": - # on découpe suivant les points virgules: - champs = val_attr.split(';') - for champ in champs: - # on découpe ensuite au 1er signe égal - variables = champ.split('=', 1) - if len(variables) == 2: - if variables[0].strip() == "charset": - # s'il y avait déjà un charset, problème - if charset != "": - raise ValueError, _(u"Double charset dans une balise META") - charset = variables[1].strip() - Journal.debug(u'trouvé attribut content = "charset=%s"' % charset) - elif nom_attr == "http-equiv" and val_attr == "content-type": - http_equiv = True - Journal.debug(u"trouvé attribut http-equiv = content-type") - # une fois parcouru tous les attributs, on regarde le résultat: - if http_equiv and charset != "": - # si un encodage différent de charset était déjà spécifié, incohérence: - if self.encodage_META != None and charset != self.encodage_META: - raise ValueError, _(u"Double encodage META incoherent.") - # idem si l'encodage du BOM et celui de la balise META sont différents: - elif self.encodage_BOM != None and charset != self.encodage_BOM: - raise ValueError, _(u"Encodages BOM et META incoherents.") - else: - self.encodage_META = charset - - -#============================================================================== -# classe HTML_Nettoyeur -#======================= -class HTML_Nettoyeur(HTMLParser_base): - """pour nettoyer un code HTML de tout code exécutable.""" - - def __init__(self, fichier_sortie=sys.stdout, encoding=None): - """constructeur pour HTML_Nettoyeur. - - fichier_sortie: fichier pour écrire la sortie. - encoding: codec à utiliser pour décoder le fichier, None sinon.""" - HTMLParser_base.__init__(self) - # fichier de sortie (objet file) - self._fdest = fichier_sortie - # flag pour indiquer qu'on est en train de supprimer une balise - self._suppression = False - # nom de la balise en cours de suppression: "script", "object", ... - self._balise_suppr = "" - # flag pour indiquer qu'un nettoyage a eu lieu, False par défaut - self.nettoyage = False - self.encoding = encoding - # flag pour indiquer qu'une balise META précisant l'encodage a été - # trouvée, et donc qu'il faut recommencer l'analyse avec le codec - # indiqué par self.encoding - self.meta_encoding = False - - def _supprimer_balise_deb(self, tag): - """pour débuter la suppression d'une balise. - tag: nom de la balise supprimée.""" - self._suppression = True - self._balise_suppr = tag - self._fdest.write(_(u"") % tag) - self.nettoyage = True - - def _supprimer_balise_fin(self, tag): - """pour terminer la suppression d'une balise. - tag: nom de la balise supprimée.""" - if self._suppression and self._balise_suppr == tag: - self._suppression = False - - - def handle_starttag(self, tag, attrs): - """balise ouvrante quelconque""" - # si c'est une balise à supprimer, on le marque: - if tag in BALISES_SUPPR: - self._supprimer_balise_deb(tag) - # si on n'est pas en mode suppression, on recopie la balise - elif not self._suppression: - # on vérifie si un des attributs commence par "on..." - # ou si une valeur d'attribut est une URL débutant par autre - # chose que "http:", "ftp:", "mailto:", ... - # dans ce cas on le supprime - suppr_attr = False # flag si attributs à supprimer - attrs_ok = [] # liste des attributs nettoyés - attrs_bruts = "" - for attr in attrs: - nom_attr = attr[0] - val_attr = attr[1] - attr_brut = attr[2] - if nom_attr.lower().startswith("on"): - # Attribut à supprimer: on ne l'ajoute pas - # à la liste des attributs nettoyés. - # On note qu'au moins un attribut est supprimé - suppr_attr = True - Journal.info2(_(u"attribut interdit: %s=...") % nom_attr) - elif ":" in val_attr: - # la valeur contient ":", ce doit être - # une URL de type "protocole:..." ou "*script:..." - # on vérifie si c'est un protocole autorisé: - url = urlparse.urlsplit(val_attr) - proto = url[0].lower() - if proto in PROTOCOLES_OK: - attrs_ok.append(attr) - attrs_bruts += attr_brut - else: - suppr_attr = True - Journal.info2(_(u"attribut interdit: %s=%s:...") % - (nom_attr, proto)) - else: - # sinon pas de problème, l'attribut est - # accepté tel quel - attrs_ok.append(attr) - attrs_bruts += attr_brut - # on reconstruit la balise nettoyée avec les attributs autorisés: -#/ balise_nettoyee = u"<%s" % tag -#/ for attr in attrs_ok: -#/ balise_nettoyee += u' %s="%s"' % (attr[0], attr[1]) -#/ balise_nettoyee += u">" - balise_nettoyee = u"<%s%s>" % (tag, attrs_bruts) - if suppr_attr: - # si au moins un attribut a été supprimé, on écrit la balise - # nettoyée: - self._fdest.write(balise_nettoyee) - self.nettoyage = True - else: - self._fdest.write(balise_nettoyee) -#/ # sinon on écrit la balise et ses attributs sous leur forme -#/ # d'origine, pour conserver au maximum la mise en page: -#/ # Mais auparavant, on vérifie la cohérence du résultat, en -#/ # comptant le nombre de signes "=", qui doit être le même: -#/ # (permet de détecter certains camouflages) -#/ debug(u"balise_nettoyee = %s" % balise_nettoyee) -#/ debug(u"self.get_starttag_text() = %s" % self.get_starttag_text()) -#/ starttag_text = self.get_starttag_text() -#/ # parfois get_starttag_text() retourne None, et cela peut lever -#/ # des exceptions... Pour l'éviter: -#/ if not starttag_text: starttag_text = "" -#/ if balise_nettoyee.count("=") != starttag_text.count("="): -#/ Journal.debug(u"Incohérence détectée lors de la reconstruction balise:") -#/ Journal.debug(u"Nb signes égal dans balise nettoyée : %d / balise d'origine: %d" -#/ % ( balise_nettoyee.count("="), starttag_text.count("="))) -#/ #raise sgmllib.SGMLParseError, "erreur de reconstruction balise" -#/ # on écrit alors la balise nettoyée: -#/ self._fdest.write(balise_nettoyee) -#/ self.nettoyage = True -#/ else: -#/ self._fdest.write(starttag_text) - - def handle_data(self, data): - """données hors balises""" - # si on n'est pas en mode suppression, on recopie les données - if not self._suppression: - self._fdest.write(data) - - def handle_endtag(self, tag): - """balise fermante quelconque""" - # si c'est une balise à supprimer, on la traite: - if tag in BALISES_SUPPR: - self._supprimer_balise_fin(tag) - # si on n'est pas en mode suppression, on recopie la balise - elif not self._suppression: - self._fdest.write("" % tag) - - def handle_charref(self, name): - """CharRef décodée dans des données entre balises. - - name est la chaîne du code entre '&#' et ';'. - """ - # si on n'est pas en mode suppression, on recopie les données - if not self._suppression: - self._fdest.write('&#'+name+';') - - def handle_entityref(self, name): - """EntityRef décodée dans des données entre balises. - - name est la chaîne du code entre '&' et ';'. - """ - # si on n'est pas en mode suppression, on recopie les données - if not self._suppression: - self._fdest.write('&'+name+';') - - def handle_comment(self, data): - """Commentaire entre ''. - """ - # si on n'est pas en mode suppression, on recopie les données - if not self._suppression: - self._fdest.write('') - - def handle_decl(self, decl): - """Décalaration SGML entre ''. - """ - # si on n'est pas en mode suppression, on recopie les données - if not self._suppression: - self._fdest.write('') - - def handle_pi(self, data): - """Processing instruction SGML entre ''. - """ - # si on n'est pas en mode suppression, on recopie les données - if not self._suppression: - self._fdest.write('') - - -#------------------------------------------------------------------------------ -# lire_BOM -#--------------------- - -def lire_BOM (debut_fichier): - """Pour déterminer si le fichier est codé en Unicode ou UTF-8 en - regardant s'il commence par un marqueur BOM (Byte Order Marker). - (sinon employer codec 'latin_1' par défaut si BOM absent) - - @param debut_fichier: chaine correspondant au début du fichier. - @return: retourne une chaine correspondant au codec à employer. - """ - for codec in BOM: - if debut_fichier.startswith(BOM[codec]): - return codec - else: - return None - - - -#------------------------------------------------------------------------------ -# classe FILTRE_HTML -#--------------------- - -class Filtre_HTML (Filtre.Filtre): - """ - classe pour un filtre de fichiers HTML. - - un objet Filtre sert à reconnaître le format d'un fichier et à - nettoyer le code éventuel qu'il contient. La classe Filtre_HTML - correspond aux documents HTML. - - @cvar nom: Le nom detaillé du filtre - @cvar nom_code: nom de code du filtre - @cvar extensions: liste des extensions de fichiers possibles - @cvar format_conteneur: indique si c'est un format conteneur - @cvar extractible: indique si il s'agit d'une archive - @cvar nettoyable: indique si il est possible de nettoyer avec ce filtre - @cvar date: date de la derniere modification du filtre - @cvar version: version du filtre - """ - - nom = _(u"Document HTML") - extensions = [".html", ".htm"] # et .php, .asp, .cgi, ... ? - format_conteneur = False - extractible = False - nettoyable = True - - # date et version définies à partir de celles du module - date = __date__ - version = __version__ - - def reconnait_format(self, fichier): - """analyse le format du fichier, et retourne True s'il correspond - au format recherché, False sinon.""" - # En théorie un fichier HTML bien formé devrait toujours commencer - # par une balise "" et se terminer par "". - # En pratique les navigateurs n'imposent aucune balise particulière, - # même un simple fichier texte est affiché comme du HTML. - # Dans un premier temps on peut vérifier quand même la présence de - # au début: ... ou pas. - #debut = fichier.lire_debut() - #if debut.startswith(""): - return True - - def nettoyer (self, fichier): - """Analyse et modifie le fichier pour supprimer tout code - exécutable qu'il peut contenir, si cela est possible. - Retourne un code résultat suivant l'action effectuée. - """ - # 1) l'encodage par défaut pour un fichier HTML est Latin-1 - encodage = "latin_1" - - # 2) on vérifie si le fichier est codé en Unicode ou UTF-8 en - # regardant s'il commence par un marqueur BOM (Byte Order Marker): - debut = fichier.lire_debut() - encodage_BOM = lire_BOM(debut) - if encodage_BOM: - encodage = encodage_BOM - Journal.info2 (_(u"encodage d'après le BOM: %s") % encodage_BOM) - - # 3) 1ère passe d'analyse pour déterminer si une balise META indique un - # autre encodage: - h = HTML_META(encodage_BOM) - # copie temporaire du fichier - copie_temp = fichier.copie_temp() - # on l'ouvre avec le codec déduit du BOM, ou Latin-1 par défaut - fich_src = codecs.open(copie_temp, 'rb', encodage) - try: - texte_source = fich_src.read(TAILLE_BUFFER) - while texte_source: - # tant qu'il y a des données à lire - # on analyse le code HTML à la recherche de balises META - h.feed(texte_source) - # on lit le bloc de données suivant - texte_source = fich_src.read(TAILLE_BUFFER) - h.close() - except HTMLParser.HTMLParseError: - # erreur de syntaxe lors de l'analyse HTML: - Journal.info2(_(u"Erreur lors de l'analyse des balises META"), exc_info=True) - erreur = str(sys.exc_info()[1]) - return self.resultat_analyse_impossible(fichier, - raison=_(u"Syntaxe HTML incorrecte"), erreur=erreur) - except ValueError, UnicodeError: - # si on obtient une de ces 2 erreurs, il s'agit de caractères - # incorrects vis-à-vis de l'encodage, ou bien d'un double encodage - # incohérent: - Journal.info2(_(u"Erreur lors de l'analyse des balises META"), exc_info=True) - erreur = str(sys.exc_info()[1]) - return self.resultat_analyse_impossible(fichier, - raison=_(u"Encodage du fichier incorrect"), erreur=erreur) - except: - # sinon on remonte l'exception: - raise - fich_src.close() - # on récupère l'encodage fourni par une balise META, si c'est le cas: - if h.encodage_META: - encodage = h.encodage_META - Journal.info2 (_(u"encodage d'après la balise META: %s") % encodage) - - # 4) 2ème passe pour nettoyer le code HTML vers un fichier temporaire - # Création d'un fichier HTML temporaire: - #f_temp, chem_temp = tempfile.mkstemp(suffix=".html", dir=Conteneur.RACINE_TEMP) -## f_temp, chem_temp = tempfile.mkstemp(suffix=".html", -## dir=commun.politique.parametres['rep_temp'].valeur) - f_temp, chem_temp = newTempFile(suffix=".html") - Journal.info2 (_(u"Fichier HTML temporaire: %s") % chem_temp) - # f_temp est un handle de fichier (cf. os.open), il faut le - # convertir en objet file: - #fich_dest = os.fdopen(f_temp, 'wb') - # si le fichier source contenait un BOM, on doit le réécrire dans le - # fichier destination avant le code HTML nettoyé: - # --> a priori pas nécessaire, ou bien seulement dans certains cas ? - #if encodage_BOM: - # os.write(f_temp, BOM[encodage_BOM]) - # Bidouille à améliorer: on ferme le fichier pour le réouvrir - # avec l'encodage correspondant à l'original: - f_temp.close() -## os.close(f_temp) - # ouverture en mode append pour écrire après le BOM éventuel: - fich_dest = codecs.open(chem_temp, 'ab', encodage) - hn = HTML_Nettoyeur(fich_dest) - # on ouvre le fichier source avec le codec déduit du BOM, d'une balise - # META, ou Latin-1 par défaut: - fich_src = codecs.open(copie_temp, 'rb', encodage) - try: - texte_source = fich_src.read(TAILLE_BUFFER) - while texte_source: - # tant qu'il y a des données à lire - # on analyse le code HTML pour le nettoyer - hn.feed(texte_source) - # et on lit le bloc de données suivant - texte_source = fich_src.read(TAILLE_BUFFER) - hn.close() - except HTMLParser.HTMLParseError: - # erreur de syntaxe lors de l'analyse HTML: - Journal.info2(_(u"Erreur lors du nettoyage HTML"), exc_info=True) - erreur = str(sys.exc_info()[1]) - return self.resultat_nettoyage_impossible(fichier, - raison=_(u"Syntaxe HTML incorrecte"), erreur=erreur) - except ValueError, UnicodeError: - # si on obtient une de ces 2 erreurs, il s'agit de caractères - # incorrects vis-à-vis de l'encodage, ou bien d'un double encodage - # incohérent: - Journal.info2(_(u"Erreur lors du nettoyage HTML"), exc_info=True) - erreur = str(sys.exc_info()[1]) - return self.resultat_nettoyage_impossible(fichier, - raison=_(u"Encodage du fichier incorrect"), erreur=erreur) - # IL FAUDRAIT AUSSI SUPPRIMER LE FICHIER TEMP SI ERREUR !! - fich_src.close() - fich_dest.close() - if hn.nettoyage: - resultat = self.resultat_nettoye(fichier) - else: - resultat = self.resultat_accepte(fichier) - # on modifie la date du nouveau fichier pour correspondre à - # celle d'origine: - date_fich = os.path.getmtime(copie_temp) - os.utime(chem_temp, (date_fich, date_fich)) - # on remplace la copie temporaire du fichier d'origine par - # la version nettoyée: - # NOTE: sous Windows on est obligé d'effacer d'abord le fichier - # d'origine, alors que sous Unix il serait simplement écrasé - fichier._copie_temp.remove() - path_temp = path(chem_temp) - path_temp.rename(copie_temp) - return resultat - - -#------------------------------------------------------------------------------ -# TESTS -#--------------------- -# tests de certaines fonctions si le module est lancé directement et non importé: -if __name__ == '__main__': - print "----------------------------------------------------------------------------" - print "Filtre HTML v%s du %s - %s" % (__version__, __date__, __author__) - print "----------------------------------------------------------------------------" - print "" - if len(sys.argv) == 3: - fich_src = file(sys.argv[1], 'rb') - fich_dest = file(sys.argv[2], 'wb') - hn = HTML_Nettoyeur(fich_dest) - hn.feed(fich_src.read()) - hn.close() - fich_src.close() - fich_dest.close() - if hn.nettoyage: - print "Le fichier HTML a ete nettoye." - else: - print "Le fichier HTML ne contenait pas d'elements actifs." - else: - print "usage: python Filtre_HTML.py " - print "" +#!/usr/bin/python +# -*- coding: iso-8859-1 -*- +""" +Filtre_HTML - ExeFilter + +Ce module contient la classe L{Filtre_HTML.Filtre_HTML} permettant de filtrer +les fichiers de type "document HTML". + +Ce fichier fait partie du projet ExeFilter. +URL du projet: U{http://www.decalage.info/exefilter} + +@organization: DGA/CELAR +@author: U{Philippe Lagadec} +@author: U{Arnaud Kerréneur} +@author: U{Tanguy Vinceleux} + +@contact: U{Philippe Lagadec} + +@copyright: DGA/CELAR 2004-2008 +@copyright: NATO/NC3A 2008-2010 (modifications PL apres ExeFilter v1.1.0) + +@license: CeCILL (open-source compatible GPL) + cf. code source ou fichier LICENCE.txt joint + +@version: 1.04 + +@status: beta +""" +#============================================================================== +__docformat__ = 'epytext en' + +__date__ = "2011-02-18" +__version__ = "1.04" + +#------------------------------------------------------------------------------ +# LICENCE pour le projet ExeFilter: + +# Copyright DGA/CELAR 2004-2008 +# Copyright NATO/NC3A 2008-2010 (PL changes after ExeFilter v1.1.0) +# Auteurs: +# - Philippe Lagadec (PL) - philippe.lagadec(a)laposte.net +# - Arnaud Kerréneur (AK) - arnaud.kerreneur(a)dga.defense.gouv.fr +# - Tanguy Vinceleux (TV) - tanguy.vinceleux(a)dga.defense.gouv.fr +# +# Ce logiciel est régi par la licence CeCILL soumise au droit français et +# respectant les principes de diffusion des logiciels libres. Vous pouvez +# utiliser, modifier et/ou redistribuer ce programme sous les conditions +# de la licence CeCILL telle que diffusée par le CEA, le CNRS et l'INRIA +# sur le site "http://www.cecill.info". Une copie de cette licence est jointe +# dans les fichiers Licence_CeCILL_V2-fr.html et Licence_CeCILL_V2-en.html. +# +# En contrepartie de l'accessibilité au code source et des droits de copie, +# de modification et de redistribution accordés par cette licence, il n'est +# offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, +# seule une responsabilité restreinte pèse sur l'auteur du programme, le +# titulaire des droits patrimoniaux et les concédants successifs. +# +# A cet égard l'attention de l'utilisateur est attirée sur les risques +# associés au chargement, à l'utilisation, à la modification et/ou au +# développement et à la reproduction du logiciel par l'utilisateur étant +# donné sa spécificité de logiciel libre, qui peut le rendre complexe à +# manipuler et qui le réserve donc à des développeurs et des professionnels +# avertis possédant des connaissances informatiques approfondies. Les +# utilisateurs sont donc invités à charger et tester l'adéquation du +# logiciel à leurs besoins dans des conditions permettant d'assurer la +# sécurité de leurs systèmes et ou de leurs données et, plus généralement, +# à l'utiliser et l'exploiter dans les mêmes conditions de sécurité. +# +# Le fait que vous puissiez accéder à cet en-tête signifie que vous avez +# pris connaissance de la licence CeCILL, et que vous en avez accepté les +# termes. + +#------------------------------------------------------------------------------ +# HISTORIQUE: +# 29/10/2004 v0.01 PL: - 1ère version +# 2004-2006 PL,AK: - evolutions +# 12/01/2007 v1.00 PL: - version 1.00 officielle +# 2008-02-24 v1.01 PL: - licence CeCILL +# 2008-03-24 v1.02 PL: - ajout de _() pour traduction gettext des chaines +# - simplification dans nettoyer() en appelant resultat_* +# 2010-02-23 v1.03 PL: - updated HTMLParser_PL import +# 2011-02-18 v1.04 PL: - fixed temp file creation using new commun functions + +#------------------------------------------------------------------------------ +# TODO: +# + option to use lxml.html or html5lib instead of HTMLParser, which is too +# picky for many normal but slightly malformed HTML pages. See: +# http://lxml.de/lxmlhtml.html +# http://lxml.de/lxmlhtml.html#cleaning-up-html +# => seems great but need to add code to report what was cleaned +# http://lxml.de/elementsoup.html +# http://lxml.de/elementsoup.html#using-soupparser-as-a-fallback +# http://lxml.de/html5parser.html +# http://wiki.whatwg.org/wiki/Sanitization_rules +# http://code.google.com/p/html5lib/wiki/UserDocumentation#Sanitizing_Tokenizer +# BUT the issue is how to be informed when html is actually sanitized... +#------------------------------------------------------------------------------ + +#=== IMPORTS ================================================================== + +# modules standards Python: +import os, sys, tempfile, codecs, traceback +from urllib.parse import urlparse as _urlparse_module +import urllib.parse as urlparse +try: + from html.parser import HTMLParser + import html.parser as HTMLParser_module +except ImportError: + import HTMLParser + +# modules du projet: +import commun +from commun import * +import Resultat, Conteneur +from . import Filtre +import thirdparty.HTMLParser_PL.HTMLParser_PL as HTMLParser_PL + + +#=== CONSTANTES =============================================================== + +# liste des protocoles autorisés dans une URL: +PROTOCOLES_OK = ["file", "http", "https", "ftp", "mailto"] + +# liste des balises HTML à supprimer: +BALISES_SUPPR = ['script', 'object', 'embed', 'applet', 'xml', 'iframe'] + +# Byte-Order Markers utilisés comme entêtes pour les encodages Unicode ou UTF-8: +BOM = { + 'utf_16_be': codecs.BOM_UTF16_BE, + 'utf_16_le': codecs.BOM_UTF16_LE, + 'utf_8' : codecs.BOM_UTF8 + } + +# nombre d'octets lus à chaque lecture du fichier +TAILLE_BUFFER = 65536 + +#=== CLASSES ================================================================== + +# Classe de base pour les parseurs HTML: +# (permet de changer facilement cette classe sans toucher au code) +HTMLParser_base = HTMLParser_PL.HTMLParser_PL +#HTMLParser_base = HTMLParser.HTMLParser + +#============================================================================== +# classe HTML_META +#===================== +class HTML_META (HTMLParser_base): + """Pour analyser un code HTML et déterminer si une balise META indique un + encodage différent de celui du BOM. + + Après analyse complète du code HTML, le résultat peut être lu dans + l'attribut encodage_META (None ou nom du codec trouvé). + Si une incohérence est détectée (encodages META et BOM différents, ou bien + deux balises META différentes), une exception ValueError est levée. + + @ivar encodage_META: encodage spécifié par une balise META. + """ + + def __init__(self, encodage_BOM=None): + """constructeur pour un objet HTML_META. + + @param encodage_BOM: nom du codec déduit du BOM, None sinon. + @type encodage_BOM: str, None + """ + # on initialise d'abord avec le constructeur de la classe de base: + HTMLParser_base.__init__(self) + # encodage déduit du BOM + self.encodage_BOM = encodage_BOM + # encodage spécifié par balise META + self.encodage_META = None + + def handle_starttag(self, tag, attrs): + """Pour traiter une balise HTML ouvrante. + Seules les balises META sont prises en compte. + """ + # on ne prend en compte que les balises META: + if tag != 'meta': return + # flag pour noter si on a un http-equiv=content-type + http_equiv = False + # charset vide au départ + charset = "" + for attr in attrs: + # conversion du nom et de la valeur de l'attribut en minuscule: + nom_attr = attr[0].lower() + val_attr = attr[1].lower() + if nom_attr == "content": + # on découpe suivant les points virgules: + champs = val_attr.split(';') + for champ in champs: + # on découpe ensuite au 1er signe égal + variables = champ.split('=', 1) + if len(variables) == 2: + if variables[0].strip() == "charset": + # s'il y avait déjà un charset, problème + if charset != "": + raise ValueError(_(u"Double charset dans une balise META")) + charset = variables[1].strip() + Journal.debug(u'trouvé attribut content = "charset=%s"' % charset) + elif nom_attr == "http-equiv" and val_attr == "content-type": + http_equiv = True + Journal.debug(u"trouvé attribut http-equiv = content-type") + # une fois parcouru tous les attributs, on regarde le résultat: + if http_equiv and charset != "": + # si un encodage différent de charset était déjà spécifié, incohérence: + if self.encodage_META != None and charset != self.encodage_META: + raise ValueError(_(u"Double encodage META incoherent.")) + # idem si l'encodage du BOM et celui de la balise META sont différents: + elif self.encodage_BOM != None and charset != self.encodage_BOM: + raise ValueError(_(u"Encodages BOM et META incoherents.")) + else: + self.encodage_META = charset + + +#============================================================================== +# classe HTML_Nettoyeur +#======================= +class HTML_Nettoyeur(HTMLParser_base): + """pour nettoyer un code HTML de tout code exécutable.""" + + def __init__(self, fichier_sortie=sys.stdout, encoding=None): + """constructeur pour HTML_Nettoyeur. + + fichier_sortie: fichier pour écrire la sortie. + encoding: codec à utiliser pour décoder le fichier, None sinon.""" + HTMLParser_base.__init__(self) + # fichier de sortie (objet file) + self._fdest = fichier_sortie + # flag pour indiquer qu'on est en train de supprimer une balise + self._suppression = False + # nom de la balise en cours de suppression: "script", "object", ... + self._balise_suppr = "" + # flag pour indiquer qu'un nettoyage a eu lieu, False par défaut + self.nettoyage = False + self.encoding = encoding + # flag pour indiquer qu'une balise META précisant l'encodage a été + # trouvée, et donc qu'il faut recommencer l'analyse avec le codec + # indiqué par self.encoding + self.meta_encoding = False + + def _supprimer_balise_deb(self, tag): + """pour débuter la suppression d'une balise. + tag: nom de la balise supprimée.""" + self._suppression = True + self._balise_suppr = tag + self._fdest.write(_(u"") % tag) + self.nettoyage = True + + def _supprimer_balise_fin(self, tag): + """pour terminer la suppression d'une balise. + tag: nom de la balise supprimée.""" + if self._suppression and self._balise_suppr == tag: + self._suppression = False + + + def handle_starttag(self, tag, attrs): + """balise ouvrante quelconque""" + # si c'est une balise à supprimer, on le marque: + if tag in BALISES_SUPPR: + self._supprimer_balise_deb(tag) + # si on n'est pas en mode suppression, on recopie la balise + elif not self._suppression: + # on vérifie si un des attributs commence par "on..." + # ou si une valeur d'attribut est une URL débutant par autre + # chose que "http:", "ftp:", "mailto:", ... + # dans ce cas on le supprime + suppr_attr = False # flag si attributs à supprimer + attrs_ok = [] # liste des attributs nettoyés + attrs_bruts = "" + for attr in attrs: + nom_attr = attr[0] + val_attr = attr[1] + attr_brut = attr[2] + if nom_attr.lower().startswith(b"on"): + # Attribut à supprimer: on ne l'ajoute pas + # à la liste des attributs nettoyés. + # On note qu'au moins un attribut est supprimé + suppr_attr = True + Journal.info2(_(u"attribut interdit: %s=...") % nom_attr) + elif ":" in val_attr: + # la valeur contient ":", ce doit être + # une URL de type "protocole:..." ou "*script:..." + # on vérifie si c'est un protocole autorisé: + url = urlparse.urlsplit(val_attr) + proto = url[0].lower() + if proto in PROTOCOLES_OK: + attrs_ok.append(attr) + attrs_bruts += attr_brut + else: + suppr_attr = True + Journal.info2(_(u"attribut interdit: %s=%s:...") % + (nom_attr, proto)) + else: + # sinon pas de problème, l'attribut est + # accepté tel quel + attrs_ok.append(attr) + attrs_bruts += attr_brut + # on reconstruit la balise nettoyée avec les attributs autorisés: +#/ balise_nettoyee = u"<%s" % tag +#/ for attr in attrs_ok: +#/ balise_nettoyee += u' %s="%s"' % (attr[0], attr[1]) +#/ balise_nettoyee += u">" + balise_nettoyee = u"<%s%s>" % (tag, attrs_bruts) + if suppr_attr: + # si au moins un attribut a été supprimé, on écrit la balise + # nettoyée: + self._fdest.write(balise_nettoyee) + self.nettoyage = True + else: + self._fdest.write(balise_nettoyee) +#/ # sinon on écrit la balise et ses attributs sous leur forme +#/ # d'origine, pour conserver au maximum la mise en page: +#/ # Mais auparavant, on vérifie la cohérence du résultat, en +#/ # comptant le nombre de signes "=", qui doit être le même: +#/ # (permet de détecter certains camouflages) +#/ debug(u"balise_nettoyee = %s" % balise_nettoyee) +#/ debug(u"self.get_starttag_text() = %s" % self.get_starttag_text()) +#/ starttag_text = self.get_starttag_text() +#/ # parfois get_starttag_text() retourne None, et cela peut lever +#/ # des exceptions... Pour l'éviter: +#/ if not starttag_text: starttag_text = "" +#/ if balise_nettoyee.count("=") != starttag_text.count("="): +#/ Journal.debug(u"Incohérence détectée lors de la reconstruction balise:") +#/ Journal.debug(u"Nb signes égal dans balise nettoyée : %d / balise d'origine: %d" +#/ % ( balise_nettoyee.count("="), starttag_text.count("="))) +#/ #raise sgmllib.SGMLParseError, "erreur de reconstruction balise" +#/ # on écrit alors la balise nettoyée: +#/ self._fdest.write(balise_nettoyee) +#/ self.nettoyage = True +#/ else: +#/ self._fdest.write(starttag_text) + + def handle_data(self, data): + """données hors balises""" + # si on n'est pas en mode suppression, on recopie les données + if not self._suppression: + self._fdest.write(data) + + def handle_endtag(self, tag): + """balise fermante quelconque""" + # si c'est une balise à supprimer, on la traite: + if tag in BALISES_SUPPR: + self._supprimer_balise_fin(tag) + # si on n'est pas en mode suppression, on recopie la balise + elif not self._suppression: + self._fdest.write("" % tag) + + def handle_charref(self, name): + """CharRef décodée dans des données entre balises. + + name est la chaîne du code entre '&#' et ';'. + """ + # si on n'est pas en mode suppression, on recopie les données + if not self._suppression: + self._fdest.write('&#'+name+';') + + def handle_entityref(self, name): + """EntityRef décodée dans des données entre balises. + + name est la chaîne du code entre '&' et ';'. + """ + # si on n'est pas en mode suppression, on recopie les données + if not self._suppression: + self._fdest.write('&'+name+';') + + def handle_comment(self, data): + """Commentaire entre ''. + """ + # si on n'est pas en mode suppression, on recopie les données + if not self._suppression: + self._fdest.write('') + + def handle_decl(self, decl): + """Décalaration SGML entre ''. + """ + # si on n'est pas en mode suppression, on recopie les données + if not self._suppression: + self._fdest.write('') + + def handle_pi(self, data): + """Processing instruction SGML entre ''. + """ + # si on n'est pas en mode suppression, on recopie les données + if not self._suppression: + self._fdest.write('') + + +#------------------------------------------------------------------------------ +# lire_BOM +#--------------------- + +def lire_BOM (debut_fichier): + """Pour déterminer si le fichier est codé en Unicode ou UTF-8 en + regardant s'il commence par un marqueur BOM (Byte Order Marker). + (sinon employer codec 'latin_1' par défaut si BOM absent) + + @param debut_fichier: chaine correspondant au début du fichier. + @return: retourne une chaine correspondant au codec à employer. + """ + for codec in BOM: + if debut_fichier.startswith(BOM[codec]): + return codec + else: + return None + + + +#------------------------------------------------------------------------------ +# classe FILTRE_HTML +#--------------------- + +class Filtre_HTML (Filtre.Filtre): + """ + classe pour un filtre de fichiers HTML. + + un objet Filtre sert à reconnaître le format d'un fichier et à + nettoyer le code éventuel qu'il contient. La classe Filtre_HTML + correspond aux documents HTML. + + @cvar nom: Le nom detaillé du filtre + @cvar nom_code: nom de code du filtre + @cvar extensions: liste des extensions de fichiers possibles + @cvar format_conteneur: indique si c'est un format conteneur + @cvar extractible: indique si il s'agit d'une archive + @cvar nettoyable: indique si il est possible de nettoyer avec ce filtre + @cvar date: date de la derniere modification du filtre + @cvar version: version du filtre + """ + + nom = _(u"Document HTML") + extensions = [".html", ".htm"] # et .php, .asp, .cgi, ... ? + format_conteneur = False + extractible = False + nettoyable = True + + # date et version définies à partir de celles du module + date = __date__ + version = __version__ + + def reconnait_format(self, fichier): + """analyse le format du fichier, et retourne True s'il correspond + au format recherché, False sinon.""" + # En théorie un fichier HTML bien formé devrait toujours commencer + # par une balise "" et se terminer par "". + # En pratique les navigateurs n'imposent aucune balise particulière, + # même un simple fichier texte est affiché comme du HTML. + # Dans un premier temps on peut vérifier quand même la présence de + # au début: ... ou pas. + #debut = fichier.lire_debut() + #if debut.startswith(b""): + return True + + def nettoyer (self, fichier): + """Analyse et modifie le fichier pour supprimer tout code + exécutable qu'il peut contenir, si cela est possible. + Retourne un code résultat suivant l'action effectuée. + """ + # 1) l'encodage par défaut pour un fichier HTML est Latin-1 + encodage = "latin_1" + + # 2) on vérifie si le fichier est codé en Unicode ou UTF-8 en + # regardant s'il commence par un marqueur BOM (Byte Order Marker): + debut = fichier.lire_debut() + encodage_BOM = lire_BOM(debut) + if encodage_BOM: + encodage = encodage_BOM + Journal.info2 (_(u"encodage d'après le BOM: %s") % encodage_BOM) + + # 3) 1ère passe d'analyse pour déterminer si une balise META indique un + # autre encodage: + h = HTML_META(encodage_BOM) + # copie temporaire du fichier + copie_temp = fichier.copie_temp() + # on l'ouvre avec le codec déduit du BOM, ou Latin-1 par défaut + fich_src = codecs.open(copie_temp, 'rb', encodage) + try: + texte_source = fich_src.read(TAILLE_BUFFER) + while texte_source: + # tant qu'il y a des données à lire + # on analyse le code HTML à la recherche de balises META + h.feed(texte_source) + # on lit le bloc de données suivant + texte_source = fich_src.read(TAILLE_BUFFER) + h.close() + except Exception: # HTMLParser.HTMLParseError removed in Python 3.5 + # erreur de syntaxe lors de l'analyse HTML: + Journal.info2(_(u"Erreur lors de l'analyse des balises META"), exc_info=True) + erreur = str(sys.exc_info()[1]) + return self.resultat_analyse_impossible(fichier, + raison=_(u"Syntaxe HTML incorrecte"), erreur=erreur) + except ValueError as UnicodeError: + # si on obtient une de ces 2 erreurs, il s'agit de caractères + # incorrects vis-à-vis de l'encodage, ou bien d'un double encodage + # incohérent: + Journal.info2(_(u"Erreur lors de l'analyse des balises META"), exc_info=True) + erreur = str(sys.exc_info()[1]) + return self.resultat_analyse_impossible(fichier, + raison=_(u"Encodage du fichier incorrect"), erreur=erreur) + except: + # sinon on remonte l'exception: + raise + fich_src.close() + # on récupère l'encodage fourni par une balise META, si c'est le cas: + if h.encodage_META: + encodage = h.encodage_META + Journal.info2 (_(u"encodage d'après la balise META: %s") % encodage) + + # 4) 2ème passe pour nettoyer le code HTML vers un fichier temporaire + # Création d'un fichier HTML temporaire: + #f_temp, chem_temp = tempfile.mkstemp(suffix=".html", dir=Conteneur.RACINE_TEMP) +## f_temp, chem_temp = tempfile.mkstemp(suffix=".html", +## dir=commun.politique.parametres['rep_temp'].valeur) + f_temp, chem_temp = newTempFile(suffix=".html") + Journal.info2 (_(u"Fichier HTML temporaire: %s") % chem_temp) + # f_temp est un handle de fichier (cf. os.open), il faut le + # convertir en objet file: + #fich_dest = os.fdopen(f_temp, 'wb') + # si le fichier source contenait un BOM, on doit le réécrire dans le + # fichier destination avant le code HTML nettoyé: + # --> a priori pas nécessaire, ou bien seulement dans certains cas ? + #if encodage_BOM: + # os.write(f_temp, BOM[encodage_BOM]) + # Bidouille à améliorer: on ferme le fichier pour le réouvrir + # avec l'encodage correspondant à l'original: + f_temp.close() +## os.close(f_temp) + # ouverture en mode append pour écrire après le BOM éventuel: + fich_dest = codecs.open(chem_temp, 'ab', encodage) + hn = HTML_Nettoyeur(fich_dest) + # on ouvre le fichier source avec le codec déduit du BOM, d'une balise + # META, ou Latin-1 par défaut: + fich_src = codecs.open(copie_temp, 'rb', encodage) + try: + texte_source = fich_src.read(TAILLE_BUFFER) + while texte_source: + # tant qu'il y a des données à lire + # on analyse le code HTML pour le nettoyer + hn.feed(texte_source) + # et on lit le bloc de données suivant + texte_source = fich_src.read(TAILLE_BUFFER) + hn.close() + except Exception: # HTMLParser.HTMLParseError removed in Python 3.5 + # erreur de syntaxe lors de l'analyse HTML: + Journal.info2(_(u"Erreur lors du nettoyage HTML"), exc_info=True) + erreur = str(sys.exc_info()[1]) + return self.resultat_nettoyage_impossible(fichier, + raison=_(u"Syntaxe HTML incorrecte"), erreur=erreur) + except ValueError as UnicodeError: + # si on obtient une de ces 2 erreurs, il s'agit de caractères + # incorrects vis-à-vis de l'encodage, ou bien d'un double encodage + # incohérent: + Journal.info2(_(u"Erreur lors du nettoyage HTML"), exc_info=True) + erreur = str(sys.exc_info()[1]) + return self.resultat_nettoyage_impossible(fichier, + raison=_(u"Encodage du fichier incorrect"), erreur=erreur) + # IL FAUDRAIT AUSSI SUPPRIMER LE FICHIER TEMP SI ERREUR !! + fich_src.close() + fich_dest.close() + if hn.nettoyage: + resultat = self.resultat_nettoye(fichier) + else: + resultat = self.resultat_accepte(fichier) + # on modifie la date du nouveau fichier pour correspondre à + # celle d'origine: + date_fich = os.path.getmtime(copie_temp) + os.utime(chem_temp, (date_fich, date_fich)) + # on remplace la copie temporaire du fichier d'origine par + # la version nettoyée: + # NOTE: sous Windows on est obligé d'effacer d'abord le fichier + # d'origine, alors que sous Unix il serait simplement écrasé + fichier._copie_temp.remove() + path_temp = path(chem_temp) + path_temp.rename(copie_temp) + return resultat + + +#------------------------------------------------------------------------------ +# TESTS +#--------------------- +# tests de certaines fonctions si le module est lancé directement et non importé: +if __name__ == '__main__': + print("----------------------------------------------------------------------------") + print("Filtre HTML v%s du %s - %s" % (__version__, __date__, __author__)) + print("----------------------------------------------------------------------------") + print("") + if len(sys.argv) == 3: + fich_src = open(sys.argv[1], 'rb') + fich_dest = open(sys.argv[2], 'wb') + hn = HTML_Nettoyeur(fich_dest) + hn.feed(fich_src.read()) + hn.close() + fich_src.close() + fich_dest.close() + if hn.nettoyage: + print("Le fichier HTML a ete nettoye.") + else: + print("Le fichier HTML ne contenait pas d'elements actifs.") + else: + print("usage: python Filtre_HTML.py ") + print("") diff --git a/Filtres/Filtre_JPEG.py b/Filtres/Filtre_JPEG.py index 9f78430..13a86a6 100644 --- a/Filtres/Filtre_JPEG.py +++ b/Filtres/Filtre_JPEG.py @@ -1,145 +1,145 @@ -#!/usr/bin/python -# -*- coding: iso-8859-1 -*- -""" -Filtre_JPEG - ExeFilter - -Ce module contient la classe L{Filtre_JPEG.Filtre_JPEG}, -pour filtrer les fichiers images JPEG. - -Ce fichier fait partie du projet ExeFilter. -URL du projet: U{http://www.decalage.info/exefilter} - -@organization: DGA/CELAR -@author: U{Philippe Lagadec} -@author: U{Arnaud Kerréneur} -@author: U{Tanguy Vinceleux} - -@contact: U{Philippe Lagadec} - -@copyright: DGA/CELAR 2004-2008 -@copyright: NATO/NC3A 2008 (modifications PL apres v1.1.0) - -@license: CeCILL (open-source compatible GPL) - cf. code source ou fichier LICENCE.txt joint - -@version: 1.02 - -@status: beta -""" -#============================================================================== -__docformat__ = 'epytext en' - -__date__ = "2008-03-24" -__version__ = "1.02" - -#------------------------------------------------------------------------------ -# LICENCE pour le projet ExeFilter: - -# Copyright DGA/CELAR 2004-2008 -# Copyright NATO/NC3A 2008 (PL changes after v1.1.0) -# Auteurs: -# - Philippe Lagadec (PL) - philippe.lagadec(a)laposte.net -# - Arnaud Kerréneur (AK) - arnaud.kerreneur(a)dga.defense.gouv.fr -# - Tanguy Vinceleux (TV) - tanguy.vinceleux(a)dga.defense.gouv.fr -# -# Ce logiciel est régi par la licence CeCILL soumise au droit français et -# respectant les principes de diffusion des logiciels libres. Vous pouvez -# utiliser, modifier et/ou redistribuer ce programme sous les conditions -# de la licence CeCILL telle que diffusée par le CEA, le CNRS et l'INRIA -# sur le site "http://www.cecill.info". Une copie de cette licence est jointe -# dans les fichiers Licence_CeCILL_V2-fr.html et Licence_CeCILL_V2-en.html. -# -# En contrepartie de l'accessibilité au code source et des droits de copie, -# de modification et de redistribution accordés par cette licence, il n'est -# offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, -# seule une responsabilité restreinte pèse sur l'auteur du programme, le -# titulaire des droits patrimoniaux et les concédants successifs. -# -# A cet égard l'attention de l'utilisateur est attirée sur les risques -# associés au chargement, à l'utilisation, à la modification et/ou au -# développement et à la reproduction du logiciel par l'utilisateur étant -# donné sa spécificité de logiciel libre, qui peut le rendre complexe à -# manipuler et qui le réserve donc à des développeurs et des professionnels -# avertis possédant des connaissances informatiques approfondies. Les -# utilisateurs sont donc invités à charger et tester l'adéquation du -# logiciel à leurs besoins dans des conditions permettant d'assurer la -# sécurité de leurs systèmes et ou de leurs données et, plus généralement, -# à l'utiliser et l'exploiter dans les mêmes conditions de sécurité. -# -# Le fait que vous puissiez accéder à cet en-tête signifie que vous avez -# pris connaissance de la licence CeCILL, et que vous en avez accepté les -# termes. - -#------------------------------------------------------------------------------ -# HISTORIQUE: -# 22/12/2004 v0.01 AK: - 1ère version -# 2004-2005 AK,PL: - evolutions -# 12/01/2007 v1.00 PL: - version 1.00 officielle -# 2007-09-20 v1.01 PL: - licence CeCILL -# 2008-03-24 v1.02 PL: - ajout de _() pour traduction gettext des chaines -# - simplification dans nettoyer() en appelant resultat_* - -# A FAIRE: -#------------------------------------------------------------------------------ - -#=== IMPORTS ================================================================== - -# modules du projet: -from commun import * -import Filtre, Resultat - -#=== CONSTANTES =============================================================== - - -#=== CLASSES ================================================================== - -#------------------------------------------------------------------------------ -# classe FILTRE_Jpeg -#------------------- -class Filtre_JPEG (Filtre.Filtre): - """ - classe pour un filtre de fichiers images Jpeg. - - un objet Filtre sert à reconnaître le format d'un fichier et à - nettoyer le code éventuel qu'il contient. La classe Filtre_Jpeg - correspond aux fichiers image JPEG. - - - @cvar nom: Le nom detaillé du filtre - @cvar nom_code: nom de code du filtre - @cvar extensions: liste des extensions de fichiers possibles - @cvar format_conteneur: indique si c'est un format conteneur - @cvar extractible: indique si il s'agit d'une archive - @cvar nettoyable: indique si il est possible de nettoyer avec ce filtre - @cvar date: date de la derniere modification du filtre - @cvar version: version du filtre - """ - - nom = _(u"Fichier Image JPEG") - extensions = [".jpg", ".jpe", ".jpeg"] - format_conteneur = False - extractible = False - nettoyable = False - - # date et version définies à partir de celles du module - date = __date__ - version = __version__ - - def reconnait_format(self, fichier): - """analyse le format du fichier, et retourne True s'il correspond - au format recherché, False sinon.""" - debut = fichier.lire_debut() - if debut.startswith("\xFF\xD8\xFF"): - return True - - def nettoyer (self, fichier): - """analyse et modifie le fichier pour supprimer tout code - exécutable qu'il peut contenir, si cela est possible. - Retourne un code résultat suivant l'action effectuée.""" - # Ce format ne contient jamais de code. - # (sauf bien sûr d'éventuels "exploits" qui sont normalement détectés - # par l'antivirus) - return self.resultat_accepte(fichier) - - - +#!/usr/bin/python +# -*- coding: iso-8859-1 -*- +""" +Filtre_JPEG - ExeFilter + +Ce module contient la classe L{Filtre_JPEG.Filtre_JPEG}, +pour filtrer les fichiers images JPEG. + +Ce fichier fait partie du projet ExeFilter. +URL du projet: U{http://www.decalage.info/exefilter} + +@organization: DGA/CELAR +@author: U{Philippe Lagadec} +@author: U{Arnaud Kerréneur} +@author: U{Tanguy Vinceleux} + +@contact: U{Philippe Lagadec} + +@copyright: DGA/CELAR 2004-2008 +@copyright: NATO/NC3A 2008 (modifications PL apres v1.1.0) + +@license: CeCILL (open-source compatible GPL) + cf. code source ou fichier LICENCE.txt joint + +@version: 1.02 + +@status: beta +""" +#============================================================================== +__docformat__ = 'epytext en' + +__date__ = "2008-03-24" +__version__ = "1.02" + +#------------------------------------------------------------------------------ +# LICENCE pour le projet ExeFilter: + +# Copyright DGA/CELAR 2004-2008 +# Copyright NATO/NC3A 2008 (PL changes after v1.1.0) +# Auteurs: +# - Philippe Lagadec (PL) - philippe.lagadec(a)laposte.net +# - Arnaud Kerréneur (AK) - arnaud.kerreneur(a)dga.defense.gouv.fr +# - Tanguy Vinceleux (TV) - tanguy.vinceleux(a)dga.defense.gouv.fr +# +# Ce logiciel est régi par la licence CeCILL soumise au droit français et +# respectant les principes de diffusion des logiciels libres. Vous pouvez +# utiliser, modifier et/ou redistribuer ce programme sous les conditions +# de la licence CeCILL telle que diffusée par le CEA, le CNRS et l'INRIA +# sur le site "http://www.cecill.info". Une copie de cette licence est jointe +# dans les fichiers Licence_CeCILL_V2-fr.html et Licence_CeCILL_V2-en.html. +# +# En contrepartie de l'accessibilité au code source et des droits de copie, +# de modification et de redistribution accordés par cette licence, il n'est +# offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, +# seule une responsabilité restreinte pèse sur l'auteur du programme, le +# titulaire des droits patrimoniaux et les concédants successifs. +# +# A cet égard l'attention de l'utilisateur est attirée sur les risques +# associés au chargement, à l'utilisation, à la modification et/ou au +# développement et à la reproduction du logiciel par l'utilisateur étant +# donné sa spécificité de logiciel libre, qui peut le rendre complexe à +# manipuler et qui le réserve donc à des développeurs et des professionnels +# avertis possédant des connaissances informatiques approfondies. Les +# utilisateurs sont donc invités à charger et tester l'adéquation du +# logiciel à leurs besoins dans des conditions permettant d'assurer la +# sécurité de leurs systèmes et ou de leurs données et, plus généralement, +# à l'utiliser et l'exploiter dans les mêmes conditions de sécurité. +# +# Le fait que vous puissiez accéder à cet en-tête signifie que vous avez +# pris connaissance de la licence CeCILL, et que vous en avez accepté les +# termes. + +#------------------------------------------------------------------------------ +# HISTORIQUE: +# 22/12/2004 v0.01 AK: - 1ère version +# 2004-2005 AK,PL: - evolutions +# 12/01/2007 v1.00 PL: - version 1.00 officielle +# 2007-09-20 v1.01 PL: - licence CeCILL +# 2008-03-24 v1.02 PL: - ajout de _() pour traduction gettext des chaines +# - simplification dans nettoyer() en appelant resultat_* + +# A FAIRE: +#------------------------------------------------------------------------------ + +#=== IMPORTS ================================================================== + +# modules du projet: +from commun import * +import Resultat +from . import Filtre +#=== CONSTANTES =============================================================== + + +#=== CLASSES ================================================================== + +#------------------------------------------------------------------------------ +# classe FILTRE_Jpeg +#------------------- +class Filtre_JPEG (Filtre.Filtre): + """ + classe pour un filtre de fichiers images Jpeg. + + un objet Filtre sert à reconnaître le format d'un fichier et à + nettoyer le code éventuel qu'il contient. La classe Filtre_Jpeg + correspond aux fichiers image JPEG. + + + @cvar nom: Le nom detaillé du filtre + @cvar nom_code: nom de code du filtre + @cvar extensions: liste des extensions de fichiers possibles + @cvar format_conteneur: indique si c'est un format conteneur + @cvar extractible: indique si il s'agit d'une archive + @cvar nettoyable: indique si il est possible de nettoyer avec ce filtre + @cvar date: date de la derniere modification du filtre + @cvar version: version du filtre + """ + + nom = _(u"Fichier Image JPEG") + extensions = [".jpg", ".jpe", ".jpeg"] + format_conteneur = False + extractible = False + nettoyable = False + + # date et version définies à partir de celles du module + date = __date__ + version = __version__ + + def reconnait_format(self, fichier): + """analyse le format du fichier, et retourne True s'il correspond + au format recherché, False sinon.""" + debut = fichier.lire_debut() + if debut.startswith(b"\xFF\xD8\xFF"): + return True + + def nettoyer (self, fichier): + """analyse et modifie le fichier pour supprimer tout code + exécutable qu'il peut contenir, si cela est possible. + Retourne un code résultat suivant l'action effectuée.""" + # Ce format ne contient jamais de code. + # (sauf bien sûr d'éventuels "exploits" qui sont normalement détectés + # par l'antivirus) + return self.resultat_accepte(fichier) + + + diff --git a/Filtres/Filtre_MP3.py b/Filtres/Filtre_MP3.py index 7d275a1..690aaca 100644 --- a/Filtres/Filtre_MP3.py +++ b/Filtres/Filtre_MP3.py @@ -1,142 +1,142 @@ -#!/usr/bin/python -# -*- coding: iso-8859-1 -*- -""" -Filtre_MP3 - ExeFilter - -Ce module contient la classe L{Filtre_MP3.Filtre_MP3}, -pour filtrer les fichiers audio MP3 (MPEG Layer III). - -Ce fichier fait partie du projet ExeFilter. -URL du projet: U{http://www.decalage.info/exefilter} - -@organization: DGA/CELAR -@author: U{Philippe Lagadec} -@author: U{Arnaud Kerréneur} -@author: U{Tanguy Vinceleux} - -@contact: U{Philippe Lagadec} - -@copyright: DGA/CELAR 2004-2007 -@license: CeCILL (open-source compatible GPL) - cf. code source ou fichier LICENCE.txt joint - -@version: 1.01 - -@status: beta -""" -__docformat__ = 'epytext en' - -__date__ = "2008-02-19" -__version__ = "1.01" - -#------------------------------------------------------------------------------ -# LICENCE pour le projet ExeFilter: - -# Copyright DGA/CELAR 2004-2007 -# Auteurs: -# - Philippe Lagadec (PL) - philippe.lagadec(a)laposte.net -# - Arnaud Kerréneur (AK) - arnaud.kerreneur(a)dga.defense.gouv.fr -# - Tanguy Vinceleux (TV) - tanguy.vinceleux(a)dga.defense.gouv.fr -# -# Ce logiciel est régi par la licence CeCILL soumise au droit français et -# respectant les principes de diffusion des logiciels libres. Vous pouvez -# utiliser, modifier et/ou redistribuer ce programme sous les conditions -# de la licence CeCILL telle que diffusée par le CEA, le CNRS et l'INRIA -# sur le site "http://www.cecill.info". Une copie de cette licence est jointe -# dans les fichiers Licence_CeCILL_V2-fr.html et Licence_CeCILL_V2-en.html. -# -# En contrepartie de l'accessibilité au code source et des droits de copie, -# de modification et de redistribution accordés par cette licence, il n'est -# offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, -# seule une responsabilité restreinte pèse sur l'auteur du programme, le -# titulaire des droits patrimoniaux et les concédants successifs. -# -# A cet égard l'attention de l'utilisateur est attirée sur les risques -# associés au chargement, à l'utilisation, à la modification et/ou au -# développement et à la reproduction du logiciel par l'utilisateur étant -# donné sa spécificité de logiciel libre, qui peut le rendre complexe à -# manipuler et qui le réserve donc à des développeurs et des professionnels -# avertis possédant des connaissances informatiques approfondies. Les -# utilisateurs sont donc invités à charger et tester l'adéquation du -# logiciel à leurs besoins dans des conditions permettant d'assurer la -# sécurité de leurs systèmes et ou de leurs données et, plus généralement, -# à l'utiliser et l'exploiter dans les mêmes conditions de sécurité. -# -# Le fait que vous puissiez accéder à cet en-tête signifie que vous avez -# pris connaissance de la licence CeCILL, et que vous en avez accepté les -# termes. - -#------------------------------------------------------------------------------ -# HISTORIQUE: -# 22/12/2004 v0.01 AK: - 1ère version -# 2004-2005 AK,PL: - evolutions -# 12/01/2007 v1.00 PL: - version 1.00 officielle -# 2007-09-20 v1.01 PL: - licence CeCILL - -# A FAIRE: -# + vérifier s'il y a d'autres tags possibles que ID3 au début d'un fichier mp3 -#------------------------------------------------------------------------------ - -#=== IMPORTS ================================================================== - -# modules du projet: -from commun import * -import Filtre, Resultat - -#=== CONSTANTES =============================================================== - - -#=== CLASSES ================================================================== - -#------------------------------------------------------------------------------ -# classe FILTRE_MP3 -#------------------- -class Filtre_MP3 (Filtre.Filtre): - """ - classe pour un filtre de fichiers son MP3. - - un objet Filtre sert à reconnaître le format d'un fichier et à - nettoyer le code éventuel qu'il contient. La classe Filtre_Mp3 - correspond aux fichiers son MP3. - - - @cvar nom: Le nom detaillé du filtre - @cvar nom_code: nom de code du filtre - @cvar extensions: liste des extensions de fichiers possibles - @cvar format_conteneur: indique si c'est un format conteneur - @cvar extractible: indique si il s'agit d'une archive - @cvar nettoyable: indique si il est possible de nettoyer avec ce filtre - @cvar date: date de la derniere modification du filtre - @cvar version: version du filtre - - - """ - - nom = "Fichier Son MP3" - extensions = [".mp3"] - format_conteneur = False - extractible = False - nettoyable = False - - # date et version définies à partir de celles du module - date = __date__ - version = __version__ - - def reconnait_format(self, fichier): - """analyse le format du fichier, et retourne True s'il correspond - au format recherché, False sinon.""" - debut = fichier.lire_debut() - if debut.startswith("\xFF\xFB") or debut.startswith("ID3"): - return True - - def nettoyer (self, fichier): - """analyse et modifie le fichier pour supprimer tout code - exécutable qu'il peut contenir, si cela est possible. - Retourne un code résultat suivant l'action effectuée.""" - # Ce format ne contient jamais de code. - # (sauf bien sûr d'éventuels "exploits" qui sont normalement détectés - # par l'antivirus) - return Resultat.Resultat(Resultat.ACCEPTE, - self.nom + " : ne contient pas de code", fichier) - - - +#!/usr/bin/python +# -*- coding: iso-8859-1 -*- +""" +Filtre_MP3 - ExeFilter + +Ce module contient la classe L{Filtre_MP3.Filtre_MP3}, +pour filtrer les fichiers audio MP3 (MPEG Layer III). + +Ce fichier fait partie du projet ExeFilter. +URL du projet: U{http://www.decalage.info/exefilter} + +@organization: DGA/CELAR +@author: U{Philippe Lagadec} +@author: U{Arnaud Kerréneur} +@author: U{Tanguy Vinceleux} + +@contact: U{Philippe Lagadec} + +@copyright: DGA/CELAR 2004-2007 +@license: CeCILL (open-source compatible GPL) - cf. code source ou fichier LICENCE.txt joint + +@version: 1.01 + +@status: beta +""" +__docformat__ = 'epytext en' + +__date__ = "2008-02-19" +__version__ = "1.01" + +#------------------------------------------------------------------------------ +# LICENCE pour le projet ExeFilter: + +# Copyright DGA/CELAR 2004-2007 +# Auteurs: +# - Philippe Lagadec (PL) - philippe.lagadec(a)laposte.net +# - Arnaud Kerréneur (AK) - arnaud.kerreneur(a)dga.defense.gouv.fr +# - Tanguy Vinceleux (TV) - tanguy.vinceleux(a)dga.defense.gouv.fr +# +# Ce logiciel est régi par la licence CeCILL soumise au droit français et +# respectant les principes de diffusion des logiciels libres. Vous pouvez +# utiliser, modifier et/ou redistribuer ce programme sous les conditions +# de la licence CeCILL telle que diffusée par le CEA, le CNRS et l'INRIA +# sur le site "http://www.cecill.info". Une copie de cette licence est jointe +# dans les fichiers Licence_CeCILL_V2-fr.html et Licence_CeCILL_V2-en.html. +# +# En contrepartie de l'accessibilité au code source et des droits de copie, +# de modification et de redistribution accordés par cette licence, il n'est +# offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, +# seule une responsabilité restreinte pèse sur l'auteur du programme, le +# titulaire des droits patrimoniaux et les concédants successifs. +# +# A cet égard l'attention de l'utilisateur est attirée sur les risques +# associés au chargement, à l'utilisation, à la modification et/ou au +# développement et à la reproduction du logiciel par l'utilisateur étant +# donné sa spécificité de logiciel libre, qui peut le rendre complexe à +# manipuler et qui le réserve donc à des développeurs et des professionnels +# avertis possédant des connaissances informatiques approfondies. Les +# utilisateurs sont donc invités à charger et tester l'adéquation du +# logiciel à leurs besoins dans des conditions permettant d'assurer la +# sécurité de leurs systèmes et ou de leurs données et, plus généralement, +# à l'utiliser et l'exploiter dans les mêmes conditions de sécurité. +# +# Le fait que vous puissiez accéder à cet en-tête signifie que vous avez +# pris connaissance de la licence CeCILL, et que vous en avez accepté les +# termes. + +#------------------------------------------------------------------------------ +# HISTORIQUE: +# 22/12/2004 v0.01 AK: - 1ère version +# 2004-2005 AK,PL: - evolutions +# 12/01/2007 v1.00 PL: - version 1.00 officielle +# 2007-09-20 v1.01 PL: - licence CeCILL + +# A FAIRE: +# + vérifier s'il y a d'autres tags possibles que ID3 au début d'un fichier mp3 +#------------------------------------------------------------------------------ + +#=== IMPORTS ================================================================== + +# modules du projet: +from commun import * +import Resultat +from . import Filtre +#=== CONSTANTES =============================================================== + + +#=== CLASSES ================================================================== + +#------------------------------------------------------------------------------ +# classe FILTRE_MP3 +#------------------- +class Filtre_MP3 (Filtre.Filtre): + """ + classe pour un filtre de fichiers son MP3. + + un objet Filtre sert à reconnaître le format d'un fichier et à + nettoyer le code éventuel qu'il contient. La classe Filtre_Mp3 + correspond aux fichiers son MP3. + + + @cvar nom: Le nom detaillé du filtre + @cvar nom_code: nom de code du filtre + @cvar extensions: liste des extensions de fichiers possibles + @cvar format_conteneur: indique si c'est un format conteneur + @cvar extractible: indique si il s'agit d'une archive + @cvar nettoyable: indique si il est possible de nettoyer avec ce filtre + @cvar date: date de la derniere modification du filtre + @cvar version: version du filtre + + + """ + + nom = "Fichier Son MP3" + extensions = [".mp3"] + format_conteneur = False + extractible = False + nettoyable = False + + # date et version définies à partir de celles du module + date = __date__ + version = __version__ + + def reconnait_format(self, fichier): + """analyse le format du fichier, et retourne True s'il correspond + au format recherché, False sinon.""" + debut = fichier.lire_debut() + if debut.startswith(b"\xFF\xFB") or debut.startswith(b"ID3"): + return True + + def nettoyer (self, fichier): + """analyse et modifie le fichier pour supprimer tout code + exécutable qu'il peut contenir, si cela est possible. + Retourne un code résultat suivant l'action effectuée.""" + # Ce format ne contient jamais de code. + # (sauf bien sûr d'éventuels "exploits" qui sont normalement détectés + # par l'antivirus) + return Resultat.Resultat(Resultat.ACCEPTE, + self.nom + " : ne contient pas de code", fichier) + + + diff --git a/Filtres/Filtre_Office.py b/Filtres/Filtre_Office.py index cb52bb3..df0c053 100644 --- a/Filtres/Filtre_Office.py +++ b/Filtres/Filtre_Office.py @@ -117,8 +117,8 @@ # modules du projet: from commun import * -import Filtre, Resultat, Parametres - +import Resultat, Parametres +from . import Filtre #=== CONSTANTES =============================================================== # types de fichiers Office: @@ -316,10 +316,10 @@ def reconnait_format(self, fichier): au format recherché, False sinon.""" debut = fichier.lire_debut() # magic très simple sur 2 octets: - #if debut.startswith("\xD0\xCF"): + #if debut.startswith(b"\xD0\xCF"): # magic plus précis d'après code trouvé dans wvWare: # A VERIFIER: cela marche-t-il pour toutes les versions d'Office ? - if debut.startswith("\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1"): + if debut.startswith(b"\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1"): # le magic est OK, on lit alors la structure OLE: # copie temporaire self.copie_temp = str_lat1(fichier.copie_temp()) @@ -348,10 +348,10 @@ def nettoyer (self, fichier): if self.parametres["detecter_ole_pkg"].valeur == True: # détection d'objet OLE Package: A AMELIORER # (éviter de lire tout le fichier en mémoire) - f = file(self.copie_temp, "rb") + f = open(self.copie_temp, "rb") buf = f.read().lower() f.close() - if 'package\x00' in buf or '\x00package' in buf: + if b'package\x00' in buf or b'\x00package' in buf: return self.resultat_nettoyage_impossible(fichier, _(u"Contient un objet OLE Package")) if self.parametres["supprimer_macros"].valeur == True: @@ -426,7 +426,7 @@ def supprimer_macros_win32 (self, fichier): Journal.debug (u"Le stream %s a ete supprime." % self.stream_macros) Journal.info2 (u"Des macros VBA ont ete trouvees et desactivees.") return self.resultat_nettoye(fichier)#, _(u"Macro(s) VBA supprimée(s)")) - except pythoncom.com_error, details: + except pythoncom.com_error as details: # exception specifique quand le stream n'existe pas if details[1] == 'STG_E_FILENOTFOUND': # macros VBA non trouvees @@ -519,10 +519,10 @@ class Filtre_Word (_Filtre_Office): # motif pour desactiver les macros: # regex du motif a rechercher (r pour raw string): # Pour Word, 'Macros' en Unicode: - regex_VBA = r'M\x00a\x00c\x00r\x00o\x00s' + regex_VBA = b'M\x00a\x00c\x00r\x00o\x00s' # chaine de remplacement: 'MacroX' # attention ici ce n'est pas une raw string, sinon les \x00 ne sont pas convertis - rempl_VBA = 'M\x00a\x00c\x00r\x00o\x00X' + rempl_VBA = b'M\x00a\x00c\x00r\x00o\x00X' motif_macros = RechercherRemplacer.Motif(regex=regex_VBA, case_sensitive=False, remplacement=rempl_VBA) @@ -574,10 +574,10 @@ class Filtre_Excel (_Filtre_Office): # motif pour desactiver les macros: # regex du motif a rechercher (r pour raw string): # Pour Excel, '_VBA_PROJECT_CUR' en Unicode: - regex_VBA = r'_\x00V\x00B\x00A\x00' + regex_VBA = b'_\x00V\x00B\x00A\x00' # chaine de remplacement: '_VBX' # attention ici ce n'est pas une raw string, sinon les \x00 ne sont pas convertis - rempl_VBA = '_\x00V\x00B\x00X\x00' + rempl_VBA = b'_\x00V\x00B\x00X\x00' motif_macros = RechercherRemplacer.Motif(regex=regex_VBA, case_sensitive=False, remplacement=rempl_VBA) diff --git a/Filtres/Filtre_OpenXML.py b/Filtres/Filtre_OpenXML.py index 62abb91..0e5a81a 100644 --- a/Filtres/Filtre_OpenXML.py +++ b/Filtres/Filtre_OpenXML.py @@ -1,168 +1,167 @@ -#!/usr/bin/python -# -*- coding: iso-8859-1 -*- -""" -Filtre_OpenXML - ExeFilter - -This module contains the class L{Filtre_OpenXML.Filtre_OpenXML}, -to scan and clean Open XML files (Microsoft Office 2007 and later). - -This file is part of the ExeFilter project. -Project URL: U{http://www.decalage.info/exefilter} - -@author: U{Philippe Lagadec} - -@contact: U{Philippe Lagadec} - -@copyright: Philippe Lagadec 2011 - -@license: CeCILL (open-source compatible GPL) - cf. code source ou fichier LICENCE.txt joint - -@version: 0.02 - -@status: alpha -""" -#============================================================================== -__docformat__ = 'epytext en' - -__date__ = "2011-12-02" -__version__ = "0.02" - -#------------------------------------------------------------------------------ -# LICENCE pour le projet ExeFilter: - -# Copyright DGA/CELAR 2004-2008 -# Copyright NATO/NC3A 2008-2010 (PL changes after v1.1.0) -# Auteurs: -# - Philippe Lagadec (PL) - philippe.lagadec(a)laposte.net -# - Arnaud Kerréneur (AK) - arnaud.kerreneur(a)dga.defense.gouv.fr -# - Tanguy Vinceleux (TV) - tanguy.vinceleux(a)dga.defense.gouv.fr -# -# Ce logiciel est régi par la licence CeCILL soumise au droit français et -# respectant les principes de diffusion des logiciels libres. Vous pouvez -# utiliser, modifier et/ou redistribuer ce programme sous les conditions -# de la licence CeCILL telle que diffusée par le CEA, le CNRS et l'INRIA -# sur le site "http://www.cecill.info". Une copie de cette licence est jointe -# dans les fichiers Licence_CeCILL_V2-fr.html et Licence_CeCILL_V2-en.html. -# -# En contrepartie de l'accessibilité au code source et des droits de copie, -# de modification et de redistribution accordés par cette licence, il n'est -# offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, -# seule une responsabilité restreinte pèse sur l'auteur du programme, le -# titulaire des droits patrimoniaux et les concédants successifs. -# -# A cet égard l'attention de l'utilisateur est attirée sur les risques -# associés au chargement, à l'utilisation, à la modification et/ou au -# développement et à la reproduction du logiciel par l'utilisateur étant -# donné sa spécificité de logiciel libre, qui peut le rendre complexe à -# manipuler et qui le réserve donc à des développeurs et des professionnels -# avertis possédant des connaissances informatiques approfondies. Les -# utilisateurs sont donc invités à charger et tester l'adéquation du -# logiciel à leurs besoins dans des conditions permettant d'assurer la -# sécurité de leurs systèmes et ou de leurs données et, plus généralement, -# à l'utiliser et l'exploiter dans les mêmes conditions de sécurité. -# -# Le fait que vous puissiez accéder à cet en-tête signifie que vous avez -# pris connaissance de la licence CeCILL, et que vous en avez accepté les -# termes. - -#------------------------------------------------------------------------------ -# CHANGELOG: -# 2011-02-21 v0.01 PL: - 1st version -# 2011-12-02 v0.02 PL: - added initial support for .rels files - -#------------------------------------------------------------------------------ -# TODO: -# - complete list of extensions (add dotx, etc) -# - split formats in several filters, one per app: Word, Excel, PowerPoint, etc? -# - filter macros here or in the container class? -# - fix relationships when a file is removed -# - add filters for WMF, EMF, TIFF, etc - -#------------------------------------------------------------------------------ - -#=== IMPORTS ================================================================== - -# modules standards Python: - -# modules du projet: -from commun import * -import Filtre_Zip -import Filtre_XML - -#=== CONSTANTES =============================================================== - - -#=== CLASSES ================================================================== - -class Filtre_OpenXML (Filtre_Zip.Filtre_Zip): - """ - class to scan and clean Open XML documents (Microsoft Office 2007 and later). - - A Filter object is used to recognize the format of a file, to scan and to - clean potential active content. - This class is based on Filtre_Zip. - - Attributes: - @cvar nom: Le nom detaillé du filtre - @cvar nom_code: nom de code du filtre - @cvar extensions: liste des extensions de fichiers possibles - @cvar format_conteneur: indique si c'est un format conteneur - @cvar extractible: indique si il s'agit d'une archive - @cvar nettoyable: indique si il est possible de nettoyer avec ce filtre - @cvar date: date de la derniere modification du filtre - @cvar version: version du filtre - """ - - nom = _(u"Open XML document") - extensions = [".docx", '.docm', '.xlsx', '.xlsm', '.pptx', '.pptm'] - format_conteneur = True - extractible = True - nettoyable = True - - # date et version définies à partir de celles du module - date = __date__ - version = __version__ - - def reconnait_format(self, fichier): - """Analyse le format du fichier, et retourne True s'il correspond - au format recherché, False sinon. - - @param fichier: fichier à analyser - @type fichier: objet L{Fichier.Fichier} - """ - #TODO: also check if mandatory files are in the archive? - return Filtre_Zip.Filtre_Zip.reconnait_format(self, fichier) - - -class Filtre_OpenXML_rels (Filtre_XML.Filtre_XML): - """ - class to scan and clean .rels files within Open XML documents (Microsoft - Office 2007 and later), which are simple XML files. - - A Filter object is used to recognize the format of a file, to scan and to - clean potential active content. - This class is based on Filtre_XML. - - Attributes: - @cvar nom: Le nom detaillé du filtre - @cvar nom_code: nom de code du filtre - @cvar extensions: liste des extensions de fichiers possibles - @cvar format_conteneur: indique si c'est un format conteneur - @cvar extractible: indique si il s'agit d'une archive - @cvar nettoyable: indique si il est possible de nettoyer avec ce filtre - @cvar date: date de la derniere modification du filtre - @cvar version: version du filtre - """ - - nom = _(u"Open XML .rels file") - extensions = ['.rels'] - - # date et version définies à partir de celles du module - date = __date__ - version = __version__ - - - - +#!/usr/bin/python +# -*- coding: iso-8859-1 -*- +""" +Filtre_OpenXML - ExeFilter + +This module contains the class L{Filtre_OpenXML.Filtre_OpenXML}, +to scan and clean Open XML files (Microsoft Office 2007 and later). + +This file is part of the ExeFilter project. +Project URL: U{http://www.decalage.info/exefilter} + +@author: U{Philippe Lagadec} + +@contact: U{Philippe Lagadec} + +@copyright: Philippe Lagadec 2011 + +@license: CeCILL (open-source compatible GPL) + cf. code source ou fichier LICENCE.txt joint + +@version: 0.02 + +@status: alpha +""" +#============================================================================== +__docformat__ = 'epytext en' + +__date__ = "2011-12-02" +__version__ = "0.02" + +#------------------------------------------------------------------------------ +# LICENCE pour le projet ExeFilter: + +# Copyright DGA/CELAR 2004-2008 +# Copyright NATO/NC3A 2008-2010 (PL changes after v1.1.0) +# Auteurs: +# - Philippe Lagadec (PL) - philippe.lagadec(a)laposte.net +# - Arnaud Kerréneur (AK) - arnaud.kerreneur(a)dga.defense.gouv.fr +# - Tanguy Vinceleux (TV) - tanguy.vinceleux(a)dga.defense.gouv.fr +# +# Ce logiciel est régi par la licence CeCILL soumise au droit français et +# respectant les principes de diffusion des logiciels libres. Vous pouvez +# utiliser, modifier et/ou redistribuer ce programme sous les conditions +# de la licence CeCILL telle que diffusée par le CEA, le CNRS et l'INRIA +# sur le site "http://www.cecill.info". Une copie de cette licence est jointe +# dans les fichiers Licence_CeCILL_V2-fr.html et Licence_CeCILL_V2-en.html. +# +# En contrepartie de l'accessibilité au code source et des droits de copie, +# de modification et de redistribution accordés par cette licence, il n'est +# offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, +# seule une responsabilité restreinte pèse sur l'auteur du programme, le +# titulaire des droits patrimoniaux et les concédants successifs. +# +# A cet égard l'attention de l'utilisateur est attirée sur les risques +# associés au chargement, à l'utilisation, à la modification et/ou au +# développement et à la reproduction du logiciel par l'utilisateur étant +# donné sa spécificité de logiciel libre, qui peut le rendre complexe à +# manipuler et qui le réserve donc à des développeurs et des professionnels +# avertis possédant des connaissances informatiques approfondies. Les +# utilisateurs sont donc invités à charger et tester l'adéquation du +# logiciel à leurs besoins dans des conditions permettant d'assurer la +# sécurité de leurs systèmes et ou de leurs données et, plus généralement, +# à l'utiliser et l'exploiter dans les mêmes conditions de sécurité. +# +# Le fait que vous puissiez accéder à cet en-tête signifie que vous avez +# pris connaissance de la licence CeCILL, et que vous en avez accepté les +# termes. + +#------------------------------------------------------------------------------ +# CHANGELOG: +# 2011-02-21 v0.01 PL: - 1st version +# 2011-12-02 v0.02 PL: - added initial support for .rels files + +#------------------------------------------------------------------------------ +# TODO: +# - complete list of extensions (add dotx, etc) +# - split formats in several filters, one per app: Word, Excel, PowerPoint, etc? +# - filter macros here or in the container class? +# - fix relationships when a file is removed +# - add filters for WMF, EMF, TIFF, etc + +#------------------------------------------------------------------------------ + +#=== IMPORTS ================================================================== + +# modules standards Python: + +# modules du projet: +from commun import * +from . import Filtre_Zip +from . import Filtre_XML +#=== CONSTANTES =============================================================== + + +#=== CLASSES ================================================================== + +class Filtre_OpenXML (Filtre_Zip.Filtre_Zip): + """ + class to scan and clean Open XML documents (Microsoft Office 2007 and later). + + A Filter object is used to recognize the format of a file, to scan and to + clean potential active content. + This class is based on Filtre_Zip. + + Attributes: + @cvar nom: Le nom detaillé du filtre + @cvar nom_code: nom de code du filtre + @cvar extensions: liste des extensions de fichiers possibles + @cvar format_conteneur: indique si c'est un format conteneur + @cvar extractible: indique si il s'agit d'une archive + @cvar nettoyable: indique si il est possible de nettoyer avec ce filtre + @cvar date: date de la derniere modification du filtre + @cvar version: version du filtre + """ + + nom = _(u"Open XML document") + extensions = [".docx", '.docm', '.xlsx', '.xlsm', '.pptx', '.pptm'] + format_conteneur = True + extractible = True + nettoyable = True + + # date et version définies à partir de celles du module + date = __date__ + version = __version__ + + def reconnait_format(self, fichier): + """Analyse le format du fichier, et retourne True s'il correspond + au format recherché, False sinon. + + @param fichier: fichier à analyser + @type fichier: objet L{Fichier.Fichier} + """ + #TODO: also check if mandatory files are in the archive? + return Filtre_Zip.Filtre_Zip.reconnait_format(self, fichier) + + +class Filtre_OpenXML_rels (Filtre_XML.Filtre_XML): + """ + class to scan and clean .rels files within Open XML documents (Microsoft + Office 2007 and later), which are simple XML files. + + A Filter object is used to recognize the format of a file, to scan and to + clean potential active content. + This class is based on Filtre_XML. + + Attributes: + @cvar nom: Le nom detaillé du filtre + @cvar nom_code: nom de code du filtre + @cvar extensions: liste des extensions de fichiers possibles + @cvar format_conteneur: indique si c'est un format conteneur + @cvar extractible: indique si il s'agit d'une archive + @cvar nettoyable: indique si il est possible de nettoyer avec ce filtre + @cvar date: date de la derniere modification du filtre + @cvar version: version du filtre + """ + + nom = _(u"Open XML .rels file") + extensions = ['.rels'] + + # date et version définies à partir de celles du module + date = __date__ + version = __version__ + + + + # coded while listening to Family of the Year's EP "TODO" \ No newline at end of file diff --git a/Filtres/Filtre_PDF.py b/Filtres/Filtre_PDF.py index ca0ce3d..37a6654 100644 --- a/Filtres/Filtre_PDF.py +++ b/Filtres/Filtre_PDF.py @@ -11,7 +11,7 @@ @organization: DGA/CELAR @author: U{Philippe Lagadec} -@author: U{Arnaud Kerréneur} +@author: U{Arnaud Kerr�neur} @author: U{Tanguy Vinceleux} @contact: U{Philippe Lagadec} @@ -39,40 +39,40 @@ # Copyright NATO/NC3A 2008-2010 (PL changes after ExeFilter v1.1.0) # Auteurs: # - Philippe Lagadec (PL) - philippe.lagadec(a)laposte.net -# - Arnaud Kerréneur (AK) - arnaud.kerreneur(a)dga.defense.gouv.fr +# - Arnaud Kerr�neur (AK) - arnaud.kerreneur(a)dga.defense.gouv.fr # - Tanguy Vinceleux (TV) - tanguy.vinceleux(a)dga.defense.gouv.fr # -# Ce logiciel est régi par la licence CeCILL soumise au droit français et +# Ce logiciel est r�gi par la licence CeCILL soumise au droit fran�ais et # respectant les principes de diffusion des logiciels libres. Vous pouvez # utiliser, modifier et/ou redistribuer ce programme sous les conditions -# de la licence CeCILL telle que diffusée par le CEA, le CNRS et l'INRIA +# de la licence CeCILL telle que diffus�e par le CEA, le CNRS et l'INRIA # sur le site "http://www.cecill.info". Une copie de cette licence est jointe # dans les fichiers Licence_CeCILL_V2-fr.html et Licence_CeCILL_V2-en.html. # -# En contrepartie de l'accessibilité au code source et des droits de copie, -# de modification et de redistribution accordés par cette licence, il n'est -# offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, -# seule une responsabilité restreinte pèse sur l'auteur du programme, le -# titulaire des droits patrimoniaux et les concédants successifs. +# En contrepartie de l'accessibilit� au code source et des droits de copie, +# de modification et de redistribution accord�s par cette licence, il n'est +# offert aux utilisateurs qu'une garantie limit�e. Pour les m�mes raisons, +# seule une responsabilit� restreinte p�se sur l'auteur du programme, le +# titulaire des droits patrimoniaux et les conc�dants successifs. # -# A cet égard l'attention de l'utilisateur est attirée sur les risques -# associés au chargement, à l'utilisation, à la modification et/ou au -# développement et à la reproduction du logiciel par l'utilisateur étant -# donné sa spécificité de logiciel libre, qui peut le rendre complexe à -# manipuler et qui le réserve donc à des développeurs et des professionnels -# avertis possédant des connaissances informatiques approfondies. Les -# utilisateurs sont donc invités à charger et tester l'adéquation du -# logiciel à leurs besoins dans des conditions permettant d'assurer la -# sécurité de leurs systèmes et ou de leurs données et, plus généralement, -# à l'utiliser et l'exploiter dans les mêmes conditions de sécurité. +# A cet �gard l'attention de l'utilisateur est attir�e sur les risques +# associ�s au chargement, � l'utilisation, � la modification et/ou au +# d�veloppement et � la reproduction du logiciel par l'utilisateur �tant +# donn� sa sp�cificit� de logiciel libre, qui peut le rendre complexe � +# manipuler et qui le r�serve donc � des d�veloppeurs et des professionnels +# avertis poss�dant des connaissances informatiques approfondies. Les +# utilisateurs sont donc invit�s � charger et tester l'ad�quation du +# logiciel � leurs besoins dans des conditions permettant d'assurer la +# s�curit� de leurs syst�mes et ou de leurs donn�es et, plus g�n�ralement, +# � l'utiliser et l'exploiter dans les m�mes conditions de s�curit�. # -# Le fait que vous puissiez accéder à cet en-tête signifie que vous avez -# pris connaissance de la licence CeCILL, et que vous en avez accepté les +# Le fait que vous puissiez acc�der � cet en-t�te signifie que vous avez +# pris connaissance de la licence CeCILL, et que vous en avez accept� les # termes. #------------------------------------------------------------------------------ # HISTORIQUE: -# 08/06/2005 v0.01 TV: - 1ère version +# 08/06/2005 v0.01 TV: - 1�re version # 2005-2006 PL,TV,AK: - evolutions # 12/01/2007 v1.00 PL: - version 1.00 officielle # 2008-02-19 v1.01 PL: - licence CeCILL @@ -136,11 +136,11 @@ class Filtre_PDF (Filtre.Filtre): """ classe pour un filtre de fichiers PDF. - un objet Filtre sert à reconnaître le format d'un fichier et à - nettoyer le code éventuel qu'il contient. La classe Filtre_PDF + un objet Filtre sert � reconna�tre le format d'un fichier et � + nettoyer le code �ventuel qu'il contient. La classe Filtre_PDF correspond aux fichiers images PDF. - @cvar nom: Le nom detaillé du filtre + @cvar nom: Le nom detaill� du filtre @cvar nom_code: nom de code du filtre @cvar extensions: liste des extensions de fichiers possibles @cvar format_conteneur: indique si c'est un format conteneur @@ -156,18 +156,18 @@ class Filtre_PDF (Filtre.Filtre): extractible = False nettoyable = True - # date et version définies à partir de celles du module + # date et version d�finies � partir de celles du module date = __date__ version = __version__ def __init__ (self, politique, parametres=None): """Constructeur d'objet Filtre_PDF. - parametres: dictionnaire pour fixer les paramètres du filtre + parametres: dictionnaire pour fixer les param�tres du filtre """ # on commence par appeler le constructeur de la classe de base Filtre.Filtre.__init__(self, politique, parametres) - # ensuite on ajoute les paramètres par défaut + # ensuite on ajoute les param�tres par d�faut # Origami: disabled by default Parametres.Parametre(u"use_origami", bool, nom=u"Remove active content using Origami engine", @@ -234,13 +234,15 @@ def __init__ (self, politique, parametres=None): def reconnait_format(self, fichier): """ analyse le format du fichier, et retourne True s'il correspond - au format recherché, False sinon. + au format recherch�, False sinon. """ debut = fichier.lire_debut() - # Un fichier PDF bien formé doit obligatoirement commencer par "%PDF-" - # (en fait Acrobat accepte jusqu'à 1019 caractères quelconques + # Un fichier PDF bien form� doit obligatoirement commencer par "%PDF-" + # (en fait Acrobat accepte jusqu'� 1019 caract�res quelconques # avant, mais ce n'est pas la structure classique d'un PDF...) - if debut.startswith("%PDF-"): + #if debut.startswith(b"%PDF-"): + # return True + if debut.find(b"%PDF-", 1, 1024): return True def clean_origami (self, fichier): @@ -382,7 +384,7 @@ def clean_simple_replace (self, fichier): fichier.remplacer_copie_temp(chem_temp) return self.resultat_nettoye(fichier) #return Resultat.Resultat(Resultat.NETTOYE, - # [self.nom + " : objets PDF actifs supprimés"], fichier) + # [self.nom + " : objets PDF actifs supprim�s"], fichier) else: # pas de contenu actif Journal.info2 (u"Aucun contenu PDF actif n'a ete trouve.") @@ -397,8 +399,8 @@ def clean_simple_replace (self, fichier): def nettoyer (self, fichier): """ analyse et modifie le fichier pour supprimer tout code - exécutable qu'il peut contenir, si cela est possible. - Retourne un code résultat suivant l'action effectuée. + ex�cutable qu'il peut contenir, si cela est possible. + Retourne un code r�sultat suivant l'action effectu�e. """ if self.parametres["use_origami"].valeur == True: try: diff --git a/Filtres/Filtre_PNG.py b/Filtres/Filtre_PNG.py index bc2cac7..f463d60 100644 --- a/Filtres/Filtre_PNG.py +++ b/Filtres/Filtre_PNG.py @@ -1,140 +1,140 @@ -#!/usr/bin/python -# -*- coding: iso-8859-1 -*- -""" -Filtre_PNG - ExeFilter - -Ce module contient la classe L{Filtre_PNG.Filtre_PNG}, -pour filtrer les fichiers images PNG. - -Ce fichier fait partie du projet ExeFilter. -URL du projet: U{http://www.decalage.info/exefilter} - -@organization: DGA/CELAR -@author: U{Philippe Lagadec} -@author: U{Arnaud Kerréneur} -@author: U{Tanguy Vinceleux} - -@contact: U{Philippe Lagadec} - -@copyright: DGA/CELAR 2004-2008 -@copyright: NATO/NC3A 2008-2009 (modifications PL apres v1.1.0) -@license: CeCILL (open-source compatible GPL) - cf. code source ou fichier LICENCE.txt joint - -@version: 1.02 - -@status: beta -""" -__docformat__ = 'epytext en' - -__date__ = "2009-10-19" -__version__ = "1.02" - -#------------------------------------------------------------------------------ -# LICENCE pour le projet ExeFilter: - -# Copyright DGA/CELAR 2004-2008, NC3A 2008-2009 -# Auteurs: -# - Philippe Lagadec (PL) - philippe.lagadec(a)laposte.net -# - Arnaud Kerréneur (AK) - arnaud.kerreneur(a)dga.defense.gouv.fr -# - Tanguy Vinceleux (TV) - tanguy.vinceleux(a)dga.defense.gouv.fr -# -# Ce logiciel est régi par la licence CeCILL soumise au droit français et -# respectant les principes de diffusion des logiciels libres. Vous pouvez -# utiliser, modifier et/ou redistribuer ce programme sous les conditions -# de la licence CeCILL telle que diffusée par le CEA, le CNRS et l'INRIA -# sur le site "http://www.cecill.info". Une copie de cette licence est jointe -# dans les fichiers Licence_CeCILL_V2-fr.html et Licence_CeCILL_V2-en.html. -# -# En contrepartie de l'accessibilité au code source et des droits de copie, -# de modification et de redistribution accordés par cette licence, il n'est -# offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, -# seule une responsabilité restreinte pèse sur l'auteur du programme, le -# titulaire des droits patrimoniaux et les concédants successifs. -# -# A cet égard l'attention de l'utilisateur est attirée sur les risques -# associés au chargement, à l'utilisation, à la modification et/ou au -# développement et à la reproduction du logiciel par l'utilisateur étant -# donné sa spécificité de logiciel libre, qui peut le rendre complexe à -# manipuler et qui le réserve donc à des développeurs et des professionnels -# avertis possédant des connaissances informatiques approfondies. Les -# utilisateurs sont donc invités à charger et tester l'adéquation du -# logiciel à leurs besoins dans des conditions permettant d'assurer la -# sécurité de leurs systèmes et ou de leurs données et, plus généralement, -# à l'utiliser et l'exploiter dans les mêmes conditions de sécurité. -# -# Le fait que vous puissiez accéder à cet en-tête signifie que vous avez -# pris connaissance de la licence CeCILL, et que vous en avez accepté les -# termes. - -#------------------------------------------------------------------------------ -# HISTORIQUE: -# 22/12/2004 v0.01 AK: - 1ère version -# 2004-2005 AK,PL: - evolutions -# 12/01/2007 v1.00 PL: - version 1.00 officielle -# 2007-09-20 v1.01 PL: - licence CeCILL -# 2009-10-19 v1.02 PL: - call self.resultat_* methods - -# A FAIRE: -# + Le filtre ne marche pas à cause du 5ème caractère (x0D = retour-chariot) -# dans l'entête. Si on s'arrête au caractère précédent, ça fonctionne. -# (bug a confirmer) -#------------------------------------------------------------------------------ - -#=== IMPORTS ================================================================== - -# modules du projet: -from commun import * -import Filtre, Resultat - -#=== CONSTANTES =============================================================== - - -#=== CLASSES ================================================================== - -#------------------------------------------------------------------------------ -# classe FILTRE_PNG -#------------------- -class Filtre_PNG (Filtre.Filtre): - """ - classe pour un filtre de fichiers images PNG. - - un objet Filtre sert à reconnaître le format d'un fichier et à - nettoyer le code éventuel qu'il contient. La classe Filtre_Png - correspond aux fichiers image PNG. - - @cvar nom: Le nom detaillé du filtre - @cvar nom_code: nom de code du filtre - @cvar extensions: liste des extensions de fichiers possibles - @cvar format_conteneur: indique si c'est un format conteneur - @cvar extractible: indique si il s'agit d'une archive - @cvar nettoyable: indique si il est possible de nettoyer avec ce filtre - @cvar date: date de la derniere modification du filtre - @cvar version: version du filtre - - """ - - nom = _(u"Fichier Image PNG") - extensions = [".png"] - format_conteneur = False - extractible = False - nettoyable = False - - # date et version définies à partir de celles du module - date = __date__ - version = __version__ - - def reconnait_format(self, fichier): - """analyse le format du fichier, et retourne True s'il correspond - au format recherché, False sinon.""" - debut = fichier.lire_debut() - if debut.startswith("\x89\x50\x4E\x47\x0D\x0A\x1A\x0A\x00\x00\x00\x0D\x49\x48\x44\x52"): - return True - - def nettoyer (self, fichier): - """analyse et modifie le fichier pour supprimer tout code - exécutable qu'il peut contenir, si cela est possible. - Retourne un code résultat suivant l'action effectuée.""" - # Ce format ne contient jamais de code. - # (sauf bien sûr d'éventuels "exploits" qui sont normalement détectés - # par l'antivirus) - return self.resultat_accepte(fichier) +#!/usr/bin/python +# -*- coding: iso-8859-1 -*- +""" +Filtre_PNG - ExeFilter + +Ce module contient la classe L{Filtre_PNG.Filtre_PNG}, +pour filtrer les fichiers images PNG. + +Ce fichier fait partie du projet ExeFilter. +URL du projet: U{http://www.decalage.info/exefilter} + +@organization: DGA/CELAR +@author: U{Philippe Lagadec} +@author: U{Arnaud Kerréneur} +@author: U{Tanguy Vinceleux} + +@contact: U{Philippe Lagadec} + +@copyright: DGA/CELAR 2004-2008 +@copyright: NATO/NC3A 2008-2009 (modifications PL apres v1.1.0) +@license: CeCILL (open-source compatible GPL) - cf. code source ou fichier LICENCE.txt joint + +@version: 1.02 + +@status: beta +""" +__docformat__ = 'epytext en' + +__date__ = "2009-10-19" +__version__ = "1.02" + +#------------------------------------------------------------------------------ +# LICENCE pour le projet ExeFilter: + +# Copyright DGA/CELAR 2004-2008, NC3A 2008-2009 +# Auteurs: +# - Philippe Lagadec (PL) - philippe.lagadec(a)laposte.net +# - Arnaud Kerréneur (AK) - arnaud.kerreneur(a)dga.defense.gouv.fr +# - Tanguy Vinceleux (TV) - tanguy.vinceleux(a)dga.defense.gouv.fr +# +# Ce logiciel est régi par la licence CeCILL soumise au droit français et +# respectant les principes de diffusion des logiciels libres. Vous pouvez +# utiliser, modifier et/ou redistribuer ce programme sous les conditions +# de la licence CeCILL telle que diffusée par le CEA, le CNRS et l'INRIA +# sur le site "http://www.cecill.info". Une copie de cette licence est jointe +# dans les fichiers Licence_CeCILL_V2-fr.html et Licence_CeCILL_V2-en.html. +# +# En contrepartie de l'accessibilité au code source et des droits de copie, +# de modification et de redistribution accordés par cette licence, il n'est +# offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, +# seule une responsabilité restreinte pèse sur l'auteur du programme, le +# titulaire des droits patrimoniaux et les concédants successifs. +# +# A cet égard l'attention de l'utilisateur est attirée sur les risques +# associés au chargement, à l'utilisation, à la modification et/ou au +# développement et à la reproduction du logiciel par l'utilisateur étant +# donné sa spécificité de logiciel libre, qui peut le rendre complexe à +# manipuler et qui le réserve donc à des développeurs et des professionnels +# avertis possédant des connaissances informatiques approfondies. Les +# utilisateurs sont donc invités à charger et tester l'adéquation du +# logiciel à leurs besoins dans des conditions permettant d'assurer la +# sécurité de leurs systèmes et ou de leurs données et, plus généralement, +# à l'utiliser et l'exploiter dans les mêmes conditions de sécurité. +# +# Le fait que vous puissiez accéder à cet en-tête signifie que vous avez +# pris connaissance de la licence CeCILL, et que vous en avez accepté les +# termes. + +#------------------------------------------------------------------------------ +# HISTORIQUE: +# 22/12/2004 v0.01 AK: - 1ère version +# 2004-2005 AK,PL: - evolutions +# 12/01/2007 v1.00 PL: - version 1.00 officielle +# 2007-09-20 v1.01 PL: - licence CeCILL +# 2009-10-19 v1.02 PL: - call self.resultat_* methods + +# A FAIRE: +# + Le filtre ne marche pas à cause du 5ème caractère (x0D = retour-chariot) +# dans l'entête. Si on s'arrête au caractère précédent, ça fonctionne. +# (bug a confirmer) +#------------------------------------------------------------------------------ + +#=== IMPORTS ================================================================== + +# modules du projet: +from commun import * +import Resultat +from . import Filtre +#=== CONSTANTES =============================================================== + + +#=== CLASSES ================================================================== + +#------------------------------------------------------------------------------ +# classe FILTRE_PNG +#------------------- +class Filtre_PNG (Filtre.Filtre): + """ + classe pour un filtre de fichiers images PNG. + + un objet Filtre sert à reconnaître le format d'un fichier et à + nettoyer le code éventuel qu'il contient. La classe Filtre_Png + correspond aux fichiers image PNG. + + @cvar nom: Le nom detaillé du filtre + @cvar nom_code: nom de code du filtre + @cvar extensions: liste des extensions de fichiers possibles + @cvar format_conteneur: indique si c'est un format conteneur + @cvar extractible: indique si il s'agit d'une archive + @cvar nettoyable: indique si il est possible de nettoyer avec ce filtre + @cvar date: date de la derniere modification du filtre + @cvar version: version du filtre + + """ + + nom = _(u"Fichier Image PNG") + extensions = [".png"] + format_conteneur = False + extractible = False + nettoyable = False + + # date et version définies à partir de celles du module + date = __date__ + version = __version__ + + def reconnait_format(self, fichier): + """analyse le format du fichier, et retourne True s'il correspond + au format recherché, False sinon.""" + debut = fichier.lire_debut() + if debut.startswith(b"\x89\x50\x4E\x47\x0D\x0A\x1A\x0A\x00\x00\x00\x0D\x49\x48\x44\x52"): + return True + + def nettoyer (self, fichier): + """analyse et modifie le fichier pour supprimer tout code + exécutable qu'il peut contenir, si cela est possible. + Retourne un code résultat suivant l'action effectuée.""" + # Ce format ne contient jamais de code. + # (sauf bien sûr d'éventuels "exploits" qui sont normalement détectés + # par l'antivirus) + return self.resultat_accepte(fichier) diff --git a/Filtres/Filtre_RTF.py b/Filtres/Filtre_RTF.py index 256c95a..07973ad 100644 --- a/Filtres/Filtre_RTF.py +++ b/Filtres/Filtre_RTF.py @@ -1,217 +1,217 @@ -#!/usr/bin/python -# -*- coding: iso-8859-1 -*- -""" -Filtre_RTF - ExeFilter - -Ce module contient la classe L{Filtre_RTF.Filtre_RTF}, -pour filtrer les documents RTF. - -Ce fichier fait partie du projet ExeFilter. -URL du projet: U{http://www.decalage.info/exefilter} - -@organization: DGA/CELAR -@author: U{Philippe Lagadec} -@author: U{Arnaud Kerréneur} -@author: U{Tanguy Vinceleux} - -@contact: U{Philippe Lagadec} - -@copyright: DGA/CELAR 2004-2008 -@copyright: NATO/NC3A 2008-2010 (modifications PL apres ExeFilter v1.1.0) - -@license: CeCILL (open-source compatible GPL) - cf. code source ou fichier LICENCE.txt joint - -@version: 1.05 - -@status: beta -""" -#============================================================================== -__docformat__ = 'epytext en' - -__date__ = "2011-02-18" -__version__ = "1.05" - -#------------------------------------------------------------------------------ -# LICENCE pour le projet ExeFilter: - -# Copyright DGA/CELAR 2004-2008 -# Copyright NATO/NC3A 2008-2010 (PL changes after ExeFilter v1.1.0) -# Auteurs: -# - Philippe Lagadec (PL) - philippe.lagadec(a)laposte.net -# - Arnaud Kerréneur (AK) - arnaud.kerreneur(a)dga.defense.gouv.fr -# - Tanguy Vinceleux (TV) - tanguy.vinceleux(a)dga.defense.gouv.fr -# -# Ce logiciel est régi par la licence CeCILL soumise au droit français et -# respectant les principes de diffusion des logiciels libres. Vous pouvez -# utiliser, modifier et/ou redistribuer ce programme sous les conditions -# de la licence CeCILL telle que diffusée par le CEA, le CNRS et l'INRIA -# sur le site "http://www.cecill.info". Une copie de cette licence est jointe -# dans les fichiers Licence_CeCILL_V2-fr.html et Licence_CeCILL_V2-en.html. -# -# En contrepartie de l'accessibilité au code source et des droits de copie, -# de modification et de redistribution accordés par cette licence, il n'est -# offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, -# seule une responsabilité restreinte pèse sur l'auteur du programme, le -# titulaire des droits patrimoniaux et les concédants successifs. -# -# A cet égard l'attention de l'utilisateur est attirée sur les risques -# associés au chargement, à l'utilisation, à la modification et/ou au -# développement et à la reproduction du logiciel par l'utilisateur étant -# donné sa spécificité de logiciel libre, qui peut le rendre complexe à -# manipuler et qui le réserve donc à des développeurs et des professionnels -# avertis possédant des connaissances informatiques approfondies. Les -# utilisateurs sont donc invités à charger et tester l'adéquation du -# logiciel à leurs besoins dans des conditions permettant d'assurer la -# sécurité de leurs systèmes et ou de leurs données et, plus généralement, -# à l'utiliser et l'exploiter dans les mêmes conditions de sécurité. -# -# Le fait que vous puissiez accéder à cet en-tête signifie que vous avez -# pris connaissance de la licence CeCILL, et que vous en avez accepté les -# termes. - -#------------------------------------------------------------------------------ -# HISTORIQUE: -# 24/10/2004 v0.01 PL: - 1ère version -# 2004-2006 PL,AK: - evolutions -# 12/01/2007 v1.00 PL: - version 1.00 officielle -# 2008-02-19 v1.01 PL: - licence CeCILL -# - ajout nettoyage objets OLE Package -# 2008-03-24 v1.02 PL: - ajout de _() pour traduction gettext des chaines -# - simplification dans nettoyer() en appelant resultat_* -# 2009-11-02 v1.03 PL: - updated parameters for gettext translation -# 2010-02-23 v1.04 PL: - updated RechercherRemplacer import -# 2011-02-18 v1.05 PL: - fixed temp file creation using new commun functions - -#------------------------------------------------------------------------------ -# A FAIRE: -#------------------------------------------------------------------------------ - -#=== IMPORTS ================================================================== - -# modules standards Python: -import os, os.path, re, binascii, tempfile - -# modules spécifiques: -import thirdparty.RechercherRemplacer.RechercherRemplacer as RechercherRemplacer - -# modules du projet: -from commun import * -import Filtre, Resultat, Parametres - -#=== FONCTIONS ================================================================ - -def _str2hexre (chaine): - """ - convertit une chaine en regex hexadecimale, case-insensitive. - (utile pour creer les motifs pour nettoyer les fichiers RTF) - """ - regex = r'' - for c in chaine: - code_lower = ord(c.lower()) - code_upper = ord(c.upper()) - regex += r'(?:%02X|%02X)' % (code_lower, code_upper) - return regex - - -#=== CONSTANTES =============================================================== - -# motif pour detecter et desactiver un objet OLE Package dans RTF: -re_RTF_Package = _str2hexre('Package') -remp_RTF_Package = binascii.hexlify('NoPackg') -motif_RTF = RechercherRemplacer.Motif(case_sensitive=False, - regex=re_RTF_Package, remplacement=remp_RTF_Package) - - -#=== CLASSES ================================================================== - -#------------------------------------------------------------------------------ -# class FILTRE_RTF -#------------------- -class Filtre_RTF (Filtre.Filtre): - """ - classe pour un filtre de fichiers RTF. - - un objet Filtre sert à reconnaître le format d'un fichier et à - nettoyer le code éventuel qu'il contient. La classe Filtre_RTF - correspond au format de document RTF de Microsoft. - - - @cvar nom: Le nom detaillé du filtre - @cvar nom_code: nom de code du filtre - @cvar extensions: liste des extensions de fichiers possibles - @cvar format_conteneur: indique si c'est un format conteneur - @cvar extractible: indique si il s'agit d'une archive - @cvar nettoyable: indique si il est possible de nettoyer avec ce filtre - @cvar date: date de la derniere modification du filtre - @cvar version: version du filtre - - """ - - nom = _(u"Document RTF") - extensions = [".rtf", ".doc"] - # Note: dans certains cas Word peut sauver un fichier RTF avec l'extension .doc - format_conteneur = False - extractible = False - nettoyable = True - - # date et version définies à partir de celles du module - date = __date__ - version = __version__ - - def __init__ (self, politique, parametres=None): - """Constructeur d'objet Filtre_RTF. - - parametres: dictionnaire pour fixer les paramètres du filtre - """ - # on commence par appeler le constructeur de la classe de base - Filtre.Filtre.__init__(self, politique, parametres) - # ensuite on ajoute les paramètres par défaut - Parametres.Parametre(u"supprimer_OLE_Package", bool, - nom=_(u"Supprimer les objets OLE Package"), - description=_(u"Supprimer tout objet OLE Package, qui peut camoufler " - "n'importe quel fichier executable."), - valeur_defaut=True).ajouter(self.parametres) - - - def reconnait_format(self, fichier): - """analyse le format du fichier, et retourne True s'il correspond - au format recherché, False sinon.""" - debut = fichier.lire_debut() - if debut.startswith("{\\rtf"): - return True - - def nettoyer (self, fichier): - """analyse et modifie le fichier pour supprimer tout code - exécutable qu'il peut contenir, si cela est possible. - Retourne un code résultat suivant l'action effectuée.""" - if self.parametres["supprimer_OLE_Package"].valeur == True: - # creation d'un nouveau fichier temporaire - f_dest, chem_temp = newTempFile() - Journal.info2 (u"Fichier temporaire: %s" % chem_temp) - # on ouvre le fichier source - f_src = open(fichier.copie_temp(), 'rb') - Journal.info2 (u"Nettoyage RTF par remplacement de chaine") - n = RechercherRemplacer.rechercherRemplacer(motifs=[motif_RTF], - fich_src=f_src, fich_dest=f_dest, taille_identique=True, controle_apres=True) - f_src.close() - f_dest.close() - if n: - Journal.info2 (u"Des objets OLE Package ont ete trouves et desactives.") - # Le fichier nettoye, on remplace la copie temporaire: - fichier.remplacer_copie_temp(chem_temp) - return self.resultat_nettoye(fichier) - #return Resultat.Resultat(Resultat.NETTOYE, - # [self.nom + " : objets OLE Package supprimés"], fichier) - else: - # pas de contenu actif - Journal.info2 (u"Aucun contenu actif n'a ete trouve.") - # on efface le ficher temporaire: - os.remove(chem_temp) - return self.resultat_accepte(fichier) - else: - resultat = self.resultat_accepte(fichier) - return resultat - - - +#!/usr/bin/python +# -*- coding: iso-8859-1 -*- +""" +Filtre_RTF - ExeFilter + +Ce module contient la classe L{Filtre_RTF.Filtre_RTF}, +pour filtrer les documents RTF. + +Ce fichier fait partie du projet ExeFilter. +URL du projet: U{http://www.decalage.info/exefilter} + +@organization: DGA/CELAR +@author: U{Philippe Lagadec} +@author: U{Arnaud Kerréneur} +@author: U{Tanguy Vinceleux} + +@contact: U{Philippe Lagadec} + +@copyright: DGA/CELAR 2004-2008 +@copyright: NATO/NC3A 2008-2010 (modifications PL apres ExeFilter v1.1.0) + +@license: CeCILL (open-source compatible GPL) + cf. code source ou fichier LICENCE.txt joint + +@version: 1.05 + +@status: beta +""" +#============================================================================== +__docformat__ = 'epytext en' + +__date__ = "2011-02-18" +__version__ = "1.05" + +#------------------------------------------------------------------------------ +# LICENCE pour le projet ExeFilter: + +# Copyright DGA/CELAR 2004-2008 +# Copyright NATO/NC3A 2008-2010 (PL changes after ExeFilter v1.1.0) +# Auteurs: +# - Philippe Lagadec (PL) - philippe.lagadec(a)laposte.net +# - Arnaud Kerréneur (AK) - arnaud.kerreneur(a)dga.defense.gouv.fr +# - Tanguy Vinceleux (TV) - tanguy.vinceleux(a)dga.defense.gouv.fr +# +# Ce logiciel est régi par la licence CeCILL soumise au droit français et +# respectant les principes de diffusion des logiciels libres. Vous pouvez +# utiliser, modifier et/ou redistribuer ce programme sous les conditions +# de la licence CeCILL telle que diffusée par le CEA, le CNRS et l'INRIA +# sur le site "http://www.cecill.info". Une copie de cette licence est jointe +# dans les fichiers Licence_CeCILL_V2-fr.html et Licence_CeCILL_V2-en.html. +# +# En contrepartie de l'accessibilité au code source et des droits de copie, +# de modification et de redistribution accordés par cette licence, il n'est +# offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, +# seule une responsabilité restreinte pèse sur l'auteur du programme, le +# titulaire des droits patrimoniaux et les concédants successifs. +# +# A cet égard l'attention de l'utilisateur est attirée sur les risques +# associés au chargement, à l'utilisation, à la modification et/ou au +# développement et à la reproduction du logiciel par l'utilisateur étant +# donné sa spécificité de logiciel libre, qui peut le rendre complexe à +# manipuler et qui le réserve donc à des développeurs et des professionnels +# avertis possédant des connaissances informatiques approfondies. Les +# utilisateurs sont donc invités à charger et tester l'adéquation du +# logiciel à leurs besoins dans des conditions permettant d'assurer la +# sécurité de leurs systèmes et ou de leurs données et, plus généralement, +# à l'utiliser et l'exploiter dans les mêmes conditions de sécurité. +# +# Le fait que vous puissiez accéder à cet en-tête signifie que vous avez +# pris connaissance de la licence CeCILL, et que vous en avez accepté les +# termes. + +#------------------------------------------------------------------------------ +# HISTORIQUE: +# 24/10/2004 v0.01 PL: - 1ère version +# 2004-2006 PL,AK: - evolutions +# 12/01/2007 v1.00 PL: - version 1.00 officielle +# 2008-02-19 v1.01 PL: - licence CeCILL +# - ajout nettoyage objets OLE Package +# 2008-03-24 v1.02 PL: - ajout de _() pour traduction gettext des chaines +# - simplification dans nettoyer() en appelant resultat_* +# 2009-11-02 v1.03 PL: - updated parameters for gettext translation +# 2010-02-23 v1.04 PL: - updated RechercherRemplacer import +# 2011-02-18 v1.05 PL: - fixed temp file creation using new commun functions + +#------------------------------------------------------------------------------ +# A FAIRE: +#------------------------------------------------------------------------------ + +#=== IMPORTS ================================================================== + +# modules standards Python: +import os, os.path, re, binascii, tempfile + +# modules spécifiques: +import thirdparty.RechercherRemplacer.RechercherRemplacer as RechercherRemplacer + +# modules du projet: +from commun import * +import Resultat, Parametres +from . import Filtre +#=== FONCTIONS ================================================================ + +def _str2hexre (chaine): + """ + convertit une chaine en regex hexadecimale, case-insensitive. + (utile pour creer les motifs pour nettoyer les fichiers RTF) + """ + regex = r'' + for c in chaine: + code_lower = ord(c.lower()) + code_upper = ord(c.upper()) + regex += r'(?:%02X|%02X)' % (code_lower, code_upper) + return regex + + +#=== CONSTANTES =============================================================== + +# motif pour detecter et desactiver un objet OLE Package dans RTF: +re_RTF_Package = _str2hexre('Package') +remp_RTF_Package = binascii.hexlify(b'NoPackg') +motif_RTF = RechercherRemplacer.Motif(case_sensitive=False, + regex=re_RTF_Package, remplacement=remp_RTF_Package) + + +#=== CLASSES ================================================================== + +#------------------------------------------------------------------------------ +# class FILTRE_RTF +#------------------- +class Filtre_RTF (Filtre.Filtre): + """ + classe pour un filtre de fichiers RTF. + + un objet Filtre sert à reconnaître le format d'un fichier et à + nettoyer le code éventuel qu'il contient. La classe Filtre_RTF + correspond au format de document RTF de Microsoft. + + + @cvar nom: Le nom detaillé du filtre + @cvar nom_code: nom de code du filtre + @cvar extensions: liste des extensions de fichiers possibles + @cvar format_conteneur: indique si c'est un format conteneur + @cvar extractible: indique si il s'agit d'une archive + @cvar nettoyable: indique si il est possible de nettoyer avec ce filtre + @cvar date: date de la derniere modification du filtre + @cvar version: version du filtre + + """ + + nom = _(u"Document RTF") + extensions = [".rtf", ".doc"] + # Note: dans certains cas Word peut sauver un fichier RTF avec l'extension .doc + format_conteneur = False + extractible = False + nettoyable = True + + # date et version définies à partir de celles du module + date = __date__ + version = __version__ + + def __init__ (self, politique, parametres=None): + """Constructeur d'objet Filtre_RTF. + + parametres: dictionnaire pour fixer les paramètres du filtre + """ + # on commence par appeler le constructeur de la classe de base + Filtre.Filtre.__init__(self, politique, parametres) + # ensuite on ajoute les paramètres par défaut + Parametres.Parametre(u"supprimer_OLE_Package", bool, + nom=_(u"Supprimer les objets OLE Package"), + description=_(u"Supprimer tout objet OLE Package, qui peut camoufler " + "n'importe quel fichier executable."), + valeur_defaut=True).ajouter(self.parametres) + + + def reconnait_format(self, fichier): + """analyse le format du fichier, et retourne True s'il correspond + au format recherché, False sinon.""" + debut = fichier.lire_debut() + if debut.startswith(b"{\\rtf"): + return True + + def nettoyer (self, fichier): + """analyse et modifie le fichier pour supprimer tout code + exécutable qu'il peut contenir, si cela est possible. + Retourne un code résultat suivant l'action effectuée.""" + if self.parametres["supprimer_OLE_Package"].valeur == True: + # creation d'un nouveau fichier temporaire + f_dest, chem_temp = newTempFile() + Journal.info2 (u"Fichier temporaire: %s" % chem_temp) + # on ouvre le fichier source + f_src = open(fichier.copie_temp(), 'rb') + Journal.info2 (u"Nettoyage RTF par remplacement de chaine") + n = RechercherRemplacer.rechercherRemplacer(motifs=[motif_RTF], + fich_src=f_src, fich_dest=f_dest, taille_identique=True, controle_apres=True) + f_src.close() + f_dest.close() + if n: + Journal.info2 (u"Des objets OLE Package ont ete trouves et desactives.") + # Le fichier nettoye, on remplace la copie temporaire: + fichier.remplacer_copie_temp(chem_temp) + return self.resultat_nettoye(fichier) + #return Resultat.Resultat(Resultat.NETTOYE, + # [self.nom + " : objets OLE Package supprimés"], fichier) + else: + # pas de contenu actif + Journal.info2 (u"Aucun contenu actif n'a ete trouve.") + # on efface le ficher temporaire: + os.remove(chem_temp) + return self.resultat_accepte(fichier) + else: + resultat = self.resultat_accepte(fichier) + return resultat + + + diff --git a/Filtres/Filtre_Texte.py b/Filtres/Filtre_Texte.py index 39b1217..4c1d3e0 100644 --- a/Filtres/Filtre_Texte.py +++ b/Filtres/Filtre_Texte.py @@ -1,171 +1,171 @@ -#!/usr/bin/python -# -*- coding: iso-8859-1 -*- -""" -Filtre_Texte - ExeFilter - -Ce module contient la classe L{Filtre_Texte.Filtre_Texte}, -pour filtrer les fichiers texte ASCII. - -Ce fichier fait partie du projet ExeFilter. -URL du projet: U{http://www.decalage.info/exefilter} - -@organization: DGA/CELAR -@author: U{Philippe Lagadec} -@author: U{Arnaud Kerréneur} -@author: U{Tanguy Vinceleux} - -@contact: U{Philippe Lagadec} - -@copyright: DGA/CELAR 2004-2008 -@copyright: NATO/NC3A 2008 (modifications PL apres v1.1.0) - -@license: CeCILL (open-source compatible GPL) - cf. code source ou fichier LICENCE.txt joint - -@version: 1.02 - -@status: beta -""" -#============================================================================== -__docformat__ = 'epytext en' - -__date__ = "2008-03-24" -__version__ = "1.02" - -#------------------------------------------------------------------------------ -# LICENCE pour le projet ExeFilter: - -# Copyright DGA/CELAR 2004-2008 -# Copyright NATO/NC3A 2008 (PL changes after v1.1.0) -# Auteurs: -# - Philippe Lagadec (PL) - philippe.lagadec(a)laposte.net -# - Arnaud Kerréneur (AK) - arnaud.kerreneur(a)dga.defense.gouv.fr -# - Tanguy Vinceleux (TV) - tanguy.vinceleux(a)dga.defense.gouv.fr -# -# Ce logiciel est régi par la licence CeCILL soumise au droit français et -# respectant les principes de diffusion des logiciels libres. Vous pouvez -# utiliser, modifier et/ou redistribuer ce programme sous les conditions -# de la licence CeCILL telle que diffusée par le CEA, le CNRS et l'INRIA -# sur le site "http://www.cecill.info". Une copie de cette licence est jointe -# dans les fichiers Licence_CeCILL_V2-fr.html et Licence_CeCILL_V2-en.html. -# -# En contrepartie de l'accessibilité au code source et des droits de copie, -# de modification et de redistribution accordés par cette licence, il n'est -# offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, -# seule une responsabilité restreinte pèse sur l'auteur du programme, le -# titulaire des droits patrimoniaux et les concédants successifs. -# -# A cet égard l'attention de l'utilisateur est attirée sur les risques -# associés au chargement, à l'utilisation, à la modification et/ou au -# développement et à la reproduction du logiciel par l'utilisateur étant -# donné sa spécificité de logiciel libre, qui peut le rendre complexe à -# manipuler et qui le réserve donc à des développeurs et des professionnels -# avertis possédant des connaissances informatiques approfondies. Les -# utilisateurs sont donc invités à charger et tester l'adéquation du -# logiciel à leurs besoins dans des conditions permettant d'assurer la -# sécurité de leurs systèmes et ou de leurs données et, plus généralement, -# à l'utiliser et l'exploiter dans les mêmes conditions de sécurité. -# -# Le fait que vous puissiez accéder à cet en-tête signifie que vous avez -# pris connaissance de la licence CeCILL, et que vous en avez accepté les -# termes. - -#------------------------------------------------------------------------------ -# HISTORIQUE: -# 24/10/2004 v0.01 PL: - 1ère version -# 2004-2005 PL: - evolutions -# 12/01/2007 v1.00 PL: - version 1.00 officielle -# 2007-09-20 v1.01 PL: - licence CeCILL -# 2008-03-24 v1.02 PL: - ajout de _() pour traduction gettext des chaines -# - simplification dans nettoyer() en appelant resultat_* - -# A FAIRE: -# + optimiser la lecture du fichier en lisant des troncons plus longs -# + optimiser l'analyse des caracteres en remplacant la boucle par des -# fonctions "builtin" plus efficaces comme strip ou translate. -# + ajouter le support d'Unicode, UTF-8 et UTF-7 ? (BOM necessaire, voir -# Filtre_HTML) -#------------------------------------------------------------------------------ - -#=== IMPORTS ================================================================== - -# modules du projet: -from commun import * -import Filtre, Resultat - -#=== CONSTANTES =============================================================== - - -#=== CLASSES ================================================================== - -#------------------------------------------------------------------------------ -# classe FILTRE_TEXTE -#------------------- -class Filtre_Texte (Filtre.Filtre): - """ - classe pour un filtre de fichiers textes ASCII. - - un objet Filtre sert à reconnaître le format d'un fichier et à - nettoyer le code éventuel qu'il contient. La classe Filtre_Texte - correspond aux fichiers texte ASCII 8 bits. - - - @cvar nom: Le nom detaillé du filtre - @cvar nom_code: nom de code du filtre - @cvar extensions: liste des extensions de fichiers possibles - @cvar format_conteneur: indique si c'est un format conteneur - @cvar extractible: indique si il s'agit d'une archive - @cvar nettoyable: indique si il est possible de nettoyer avec ce filtre - @cvar date: date de la derniere modification du filtre - @cvar version: version du filtre - - """ - - nom = _(u"Fichier Texte ASCII") - extensions = [".txt", ".doc", "", ".ini", ".inf"] - format_conteneur = False - extractible = False - nettoyable = False - - # date et version définies à partir de celles du module - date = __date__ - version = __version__ - - # table des 256 caractères ASCII autorisés - table_ascii = 256 * [True] - table_ascii [0:32] = [False]*32 # caractères non imprimables - table_ascii [8] = True # backspace (parfois utilisé ?) - table_ascii [9] = True # tab - table_ascii [10] = True # saut de ligne - table_ascii [12] = True # saut de page - table_ascii [13] = True # retour chariot - # REMARQUE: Mailsweeper est plus restrictif, pour lui les caractères de - # texte sont seulement 9, 10, 13, 32-126 et 160-255 ! - - def reconnait_format(self, fichier): - """analyse le format du fichier, et retourne True s'il correspond - au format recherché, False sinon.""" - # on ouvre la copie temporaire du fichier - f = open(fichier.copie_temp(), 'rb') - # on lit le fichier 1 octet par un octet - c = f.read(1) - # tant qu'on récupère 1 octet on n'est pas à la fin du fichier - while c != "": - # on récupère le code ASCII de l'octet - code_ascii = ord(c) - # on regarde si c est un caractère texte ASCII - if code_ascii>255 or not self.table_ascii[code_ascii]: - # on arrête dès qu'on trouve un caractère non-ASCII - return False - # on lit l'octet suivant - c=f.read(1) - f.close() - return True - - def nettoyer (self, fichier): - """analyse et modifie le fichier pour supprimer tout code - exécutable qu'il peut contenir, si cela est possible. - Retourne un code résultat suivant l'action effectuée.""" - # ce format ne contient jamais de code - return self.resultat_accepte(fichier) - +#!/usr/bin/python +# -*- coding: iso-8859-1 -*- +""" +Filtre_Texte - ExeFilter + +Ce module contient la classe L{Filtre_Texte.Filtre_Texte}, +pour filtrer les fichiers texte ASCII. + +Ce fichier fait partie du projet ExeFilter. +URL du projet: U{http://www.decalage.info/exefilter} + +@organization: DGA/CELAR +@author: U{Philippe Lagadec} +@author: U{Arnaud Kerréneur} +@author: U{Tanguy Vinceleux} + +@contact: U{Philippe Lagadec} + +@copyright: DGA/CELAR 2004-2008 +@copyright: NATO/NC3A 2008 (modifications PL apres v1.1.0) + +@license: CeCILL (open-source compatible GPL) + cf. code source ou fichier LICENCE.txt joint + +@version: 1.02 + +@status: beta +""" +#============================================================================== +__docformat__ = 'epytext en' + +__date__ = "2008-03-24" +__version__ = "1.02" + +#------------------------------------------------------------------------------ +# LICENCE pour le projet ExeFilter: + +# Copyright DGA/CELAR 2004-2008 +# Copyright NATO/NC3A 2008 (PL changes after v1.1.0) +# Auteurs: +# - Philippe Lagadec (PL) - philippe.lagadec(a)laposte.net +# - Arnaud Kerréneur (AK) - arnaud.kerreneur(a)dga.defense.gouv.fr +# - Tanguy Vinceleux (TV) - tanguy.vinceleux(a)dga.defense.gouv.fr +# +# Ce logiciel est régi par la licence CeCILL soumise au droit français et +# respectant les principes de diffusion des logiciels libres. Vous pouvez +# utiliser, modifier et/ou redistribuer ce programme sous les conditions +# de la licence CeCILL telle que diffusée par le CEA, le CNRS et l'INRIA +# sur le site "http://www.cecill.info". Une copie de cette licence est jointe +# dans les fichiers Licence_CeCILL_V2-fr.html et Licence_CeCILL_V2-en.html. +# +# En contrepartie de l'accessibilité au code source et des droits de copie, +# de modification et de redistribution accordés par cette licence, il n'est +# offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, +# seule une responsabilité restreinte pèse sur l'auteur du programme, le +# titulaire des droits patrimoniaux et les concédants successifs. +# +# A cet égard l'attention de l'utilisateur est attirée sur les risques +# associés au chargement, à l'utilisation, à la modification et/ou au +# développement et à la reproduction du logiciel par l'utilisateur étant +# donné sa spécificité de logiciel libre, qui peut le rendre complexe à +# manipuler et qui le réserve donc à des développeurs et des professionnels +# avertis possédant des connaissances informatiques approfondies. Les +# utilisateurs sont donc invités à charger et tester l'adéquation du +# logiciel à leurs besoins dans des conditions permettant d'assurer la +# sécurité de leurs systèmes et ou de leurs données et, plus généralement, +# à l'utiliser et l'exploiter dans les mêmes conditions de sécurité. +# +# Le fait que vous puissiez accéder à cet en-tête signifie que vous avez +# pris connaissance de la licence CeCILL, et que vous en avez accepté les +# termes. + +#------------------------------------------------------------------------------ +# HISTORIQUE: +# 24/10/2004 v0.01 PL: - 1ère version +# 2004-2005 PL: - evolutions +# 12/01/2007 v1.00 PL: - version 1.00 officielle +# 2007-09-20 v1.01 PL: - licence CeCILL +# 2008-03-24 v1.02 PL: - ajout de _() pour traduction gettext des chaines +# - simplification dans nettoyer() en appelant resultat_* + +# A FAIRE: +# + optimiser la lecture du fichier en lisant des troncons plus longs +# + optimiser l'analyse des caracteres en remplacant la boucle par des +# fonctions "builtin" plus efficaces comme strip ou translate. +# + ajouter le support d'Unicode, UTF-8 et UTF-7 ? (BOM necessaire, voir +# Filtre_HTML) +#------------------------------------------------------------------------------ + +#=== IMPORTS ================================================================== + +# modules du projet: +from commun import * +import Resultat +from . import Filtre +#=== CONSTANTES =============================================================== + + +#=== CLASSES ================================================================== + +#------------------------------------------------------------------------------ +# classe FILTRE_TEXTE +#------------------- +class Filtre_Texte (Filtre.Filtre): + """ + classe pour un filtre de fichiers textes ASCII. + + un objet Filtre sert à reconnaître le format d'un fichier et à + nettoyer le code éventuel qu'il contient. La classe Filtre_Texte + correspond aux fichiers texte ASCII 8 bits. + + + @cvar nom: Le nom detaillé du filtre + @cvar nom_code: nom de code du filtre + @cvar extensions: liste des extensions de fichiers possibles + @cvar format_conteneur: indique si c'est un format conteneur + @cvar extractible: indique si il s'agit d'une archive + @cvar nettoyable: indique si il est possible de nettoyer avec ce filtre + @cvar date: date de la derniere modification du filtre + @cvar version: version du filtre + + """ + + nom = _(u"Fichier Texte ASCII") + extensions = [".txt", ".doc", "", ".ini", ".inf"] + format_conteneur = False + extractible = False + nettoyable = False + + # date et version définies à partir de celles du module + date = __date__ + version = __version__ + + # table des 256 caractères ASCII autorisés + table_ascii = 256 * [True] + table_ascii [0:32] = [False]*32 # caractères non imprimables + table_ascii [8] = True # backspace (parfois utilisé ?) + table_ascii [9] = True # tab + table_ascii [10] = True # saut de ligne + table_ascii [12] = True # saut de page + table_ascii [13] = True # retour chariot + # REMARQUE: Mailsweeper est plus restrictif, pour lui les caractères de + # texte sont seulement 9, 10, 13, 32-126 et 160-255 ! + + def reconnait_format(self, fichier): + """analyse le format du fichier, et retourne True s'il correspond + au format recherché, False sinon.""" + # on ouvre la copie temporaire du fichier + f = open(fichier.copie_temp(), 'rb') + # on lit le fichier 1 octet par un octet + c = f.read(1) + # tant qu'on récupère 1 octet on n'est pas à la fin du fichier + while c != b"": + # on récupère le code ASCII de l'octet + code_ascii = ord(c) + # on regarde si c est un caractère texte ASCII + if code_ascii>255 or not self.table_ascii[code_ascii]: + # on arrête dès qu'on trouve un caractère non-ASCII + return False + # on lit l'octet suivant + c=f.read(1) + f.close() + return True + + def nettoyer (self, fichier): + """analyse et modifie le fichier pour supprimer tout code + exécutable qu'il peut contenir, si cela est possible. + Retourne un code résultat suivant l'action effectuée.""" + # ce format ne contient jamais de code + return self.resultat_accepte(fichier) + diff --git a/Filtres/Filtre_WAV.py b/Filtres/Filtre_WAV.py index 0c72326..e9c541d 100644 --- a/Filtres/Filtre_WAV.py +++ b/Filtres/Filtre_WAV.py @@ -1,139 +1,139 @@ -#!/usr/bin/python -# -*- coding: iso-8859-1 -*- -""" -Filtre_WAV - ExeFilter - -Ce module contient la classe L{Filtre_WAV.Filtre_WAV}, -pour filtrer les fichiers audio WAV. - -Ce fichier fait partie du projet ExeFilter. -URL du projet: U{http://www.decalage.info/exefilter} - -@organization: DGA/CELAR -@author: U{Philippe Lagadec} -@author: U{Arnaud Kerréneur} -@author: U{Tanguy Vinceleux} - -@contact: U{Philippe Lagadec} - -@copyright: DGA/CELAR 2004-2007 -@license: CeCILL (open-source compatible GPL) - cf. code source ou fichier LICENCE.txt joint - -@version: 1.01 - -@status: beta -""" -__docformat__ = 'epytext en' - -__date__ = "2008-02-19" -__version__ = "1.01" - -#------------------------------------------------------------------------------ -# LICENCE pour le projet ExeFilter: - -# Copyright DGA/CELAR 2004-2007 -# Auteurs: -# - Philippe Lagadec (PL) - philippe.lagadec(a)laposte.net -# - Arnaud Kerréneur (AK) - arnaud.kerreneur(a)dga.defense.gouv.fr -# - Tanguy Vinceleux (TV) - tanguy.vinceleux(a)dga.defense.gouv.fr -# -# Ce logiciel est régi par la licence CeCILL soumise au droit français et -# respectant les principes de diffusion des logiciels libres. Vous pouvez -# utiliser, modifier et/ou redistribuer ce programme sous les conditions -# de la licence CeCILL telle que diffusée par le CEA, le CNRS et l'INRIA -# sur le site "http://www.cecill.info". Une copie de cette licence est jointe -# dans les fichiers Licence_CeCILL_V2-fr.html et Licence_CeCILL_V2-en.html. -# -# En contrepartie de l'accessibilité au code source et des droits de copie, -# de modification et de redistribution accordés par cette licence, il n'est -# offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, -# seule une responsabilité restreinte pèse sur l'auteur du programme, le -# titulaire des droits patrimoniaux et les concédants successifs. -# -# A cet égard l'attention de l'utilisateur est attirée sur les risques -# associés au chargement, à l'utilisation, à la modification et/ou au -# développement et à la reproduction du logiciel par l'utilisateur étant -# donné sa spécificité de logiciel libre, qui peut le rendre complexe à -# manipuler et qui le réserve donc à des développeurs et des professionnels -# avertis possédant des connaissances informatiques approfondies. Les -# utilisateurs sont donc invités à charger et tester l'adéquation du -# logiciel à leurs besoins dans des conditions permettant d'assurer la -# sécurité de leurs systèmes et ou de leurs données et, plus généralement, -# à l'utiliser et l'exploiter dans les mêmes conditions de sécurité. -# -# Le fait que vous puissiez accéder à cet en-tête signifie que vous avez -# pris connaissance de la licence CeCILL, et que vous en avez accepté les -# termes. - -#------------------------------------------------------------------------------ -# HISTORIQUE: -# 22/12/2004 v0.01 TV: - 1ère version -# 2004-2005 TV,PL: - evolutions -# 12/01/2007 v1.00 PL: - version 1.00 officielle -# 2007-09-20 v1.01 PL: - licence CeCILL - -# A FAIRE: -# + Tenir compte des octets indiquant la taille du fichier; -#------------------------------------------------------------------------------ - -#=== IMPORTS ================================================================== - -# modules du projet: -from commun import * -import Filtre, Resultat - -#=== CONSTANTES =============================================================== - - -#=== CLASSES ================================================================== - -#------------------------------------------------------------------------------ -# classe FILTRE_WAV -#------------------- -class Filtre_WAV (Filtre.Filtre): - """ - classe pour un filtre de fichiers WAV. - - un objet Filtre sert à reconnaître le format d'un fichier et à - nettoyer le code éventuel qu'il contient. La classe Filtre_Texte - correspond aux fichiers texte ASCII 8 bits. - - - @cvar nom: Le nom detaillé du filtre - @cvar nom_code: nom de code du filtre - @cvar extensions: liste des extensions de fichiers possibles - @cvar format_conteneur: indique si c'est un format conteneur - @cvar extractible: indique si il s'agit d'une archive - @cvar nettoyable: indique si il est possible de nettoyer avec ce filtre - @cvar date: date de la derniere modification du filtre - @cvar version: version du filtre - - - """ - - nom = "Fichier son WAV" - extensions = [".wav"] - format_conteneur = False - extractible = False - nettoyable = False - - # date et version définies à partir de celles du module - date = __date__ - version = __version__ - - def reconnait_format(self, fichier): - """analyse le format du fichier, et retourne True s'il correspond - au format recherché, False sinon.""" - debut = fichier.lire_debut() - if debut.startswith("\x52\x49\x46\x46") and debut[8:12]==("\x57\x41\x56\x45"): - return True - - def nettoyer (self, fichier): - """analyse et modifie le fichier pour supprimer tout code - exécutable qu'il peut contenir, si cela est possible. - Retourne un code résultat suivant l'action effectuée.""" - # Ce format ne contient jamais de code. - # (sauf bien sûr d'éventuels "exploits" qui sont normalement détectés - # par l'antivirus) - return Resultat.Resultat(Resultat.ACCEPTE, - self.nom + " : ne contient pas de code", fichier) +#!/usr/bin/python +# -*- coding: iso-8859-1 -*- +""" +Filtre_WAV - ExeFilter + +Ce module contient la classe L{Filtre_WAV.Filtre_WAV}, +pour filtrer les fichiers audio WAV. + +Ce fichier fait partie du projet ExeFilter. +URL du projet: U{http://www.decalage.info/exefilter} + +@organization: DGA/CELAR +@author: U{Philippe Lagadec} +@author: U{Arnaud Kerréneur} +@author: U{Tanguy Vinceleux} + +@contact: U{Philippe Lagadec} + +@copyright: DGA/CELAR 2004-2007 +@license: CeCILL (open-source compatible GPL) - cf. code source ou fichier LICENCE.txt joint + +@version: 1.01 + +@status: beta +""" +__docformat__ = 'epytext en' + +__date__ = "2008-02-19" +__version__ = "1.01" + +#------------------------------------------------------------------------------ +# LICENCE pour le projet ExeFilter: + +# Copyright DGA/CELAR 2004-2007 +# Auteurs: +# - Philippe Lagadec (PL) - philippe.lagadec(a)laposte.net +# - Arnaud Kerréneur (AK) - arnaud.kerreneur(a)dga.defense.gouv.fr +# - Tanguy Vinceleux (TV) - tanguy.vinceleux(a)dga.defense.gouv.fr +# +# Ce logiciel est régi par la licence CeCILL soumise au droit français et +# respectant les principes de diffusion des logiciels libres. Vous pouvez +# utiliser, modifier et/ou redistribuer ce programme sous les conditions +# de la licence CeCILL telle que diffusée par le CEA, le CNRS et l'INRIA +# sur le site "http://www.cecill.info". Une copie de cette licence est jointe +# dans les fichiers Licence_CeCILL_V2-fr.html et Licence_CeCILL_V2-en.html. +# +# En contrepartie de l'accessibilité au code source et des droits de copie, +# de modification et de redistribution accordés par cette licence, il n'est +# offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, +# seule une responsabilité restreinte pèse sur l'auteur du programme, le +# titulaire des droits patrimoniaux et les concédants successifs. +# +# A cet égard l'attention de l'utilisateur est attirée sur les risques +# associés au chargement, à l'utilisation, à la modification et/ou au +# développement et à la reproduction du logiciel par l'utilisateur étant +# donné sa spécificité de logiciel libre, qui peut le rendre complexe à +# manipuler et qui le réserve donc à des développeurs et des professionnels +# avertis possédant des connaissances informatiques approfondies. Les +# utilisateurs sont donc invités à charger et tester l'adéquation du +# logiciel à leurs besoins dans des conditions permettant d'assurer la +# sécurité de leurs systèmes et ou de leurs données et, plus généralement, +# à l'utiliser et l'exploiter dans les mêmes conditions de sécurité. +# +# Le fait que vous puissiez accéder à cet en-tête signifie que vous avez +# pris connaissance de la licence CeCILL, et que vous en avez accepté les +# termes. + +#------------------------------------------------------------------------------ +# HISTORIQUE: +# 22/12/2004 v0.01 TV: - 1ère version +# 2004-2005 TV,PL: - evolutions +# 12/01/2007 v1.00 PL: - version 1.00 officielle +# 2007-09-20 v1.01 PL: - licence CeCILL + +# A FAIRE: +# + Tenir compte des octets indiquant la taille du fichier; +#------------------------------------------------------------------------------ + +#=== IMPORTS ================================================================== + +# modules du projet: +from commun import * +import Resultat +from . import Filtre +#=== CONSTANTES =============================================================== + + +#=== CLASSES ================================================================== + +#------------------------------------------------------------------------------ +# classe FILTRE_WAV +#------------------- +class Filtre_WAV (Filtre.Filtre): + """ + classe pour un filtre de fichiers WAV. + + un objet Filtre sert à reconnaître le format d'un fichier et à + nettoyer le code éventuel qu'il contient. La classe Filtre_Texte + correspond aux fichiers texte ASCII 8 bits. + + + @cvar nom: Le nom detaillé du filtre + @cvar nom_code: nom de code du filtre + @cvar extensions: liste des extensions de fichiers possibles + @cvar format_conteneur: indique si c'est un format conteneur + @cvar extractible: indique si il s'agit d'une archive + @cvar nettoyable: indique si il est possible de nettoyer avec ce filtre + @cvar date: date de la derniere modification du filtre + @cvar version: version du filtre + + + """ + + nom = "Fichier son WAV" + extensions = [".wav"] + format_conteneur = False + extractible = False + nettoyable = False + + # date et version définies à partir de celles du module + date = __date__ + version = __version__ + + def reconnait_format(self, fichier): + """analyse le format du fichier, et retourne True s'il correspond + au format recherché, False sinon.""" + debut = fichier.lire_debut() + if debut.startswith(b"\x52\x49\x46\x46") and debut[8:12]==(b"\x57\x41\x56\x45"): + return True + + def nettoyer (self, fichier): + """analyse et modifie le fichier pour supprimer tout code + exécutable qu'il peut contenir, si cela est possible. + Retourne un code résultat suivant l'action effectuée.""" + # Ce format ne contient jamais de code. + # (sauf bien sûr d'éventuels "exploits" qui sont normalement détectés + # par l'antivirus) + return Resultat.Resultat(Resultat.ACCEPTE, + self.nom + " : ne contient pas de code", fichier) diff --git a/Filtres/Filtre_XML.py b/Filtres/Filtre_XML.py index 186abcd..812eff1 100644 --- a/Filtres/Filtre_XML.py +++ b/Filtres/Filtre_XML.py @@ -1,210 +1,210 @@ -#!/usr/bin/python -# -*- coding: iso-8859-1 -*- -""" -Filtre_XML - ExeFilter - -Ce module contient la classe L{Filtre_XML.Filtre_XML} permettant de filtrer -les fichiers de type "document XML". - -Ce fichier fait partie du projet ExeFilter. -URL du projet: U{http://www.decalage.info/exefilter} - -@author: U{Philippe Lagadec} - -@contact: U{Philippe Lagadec} - -@copyright: Philippe Lagadec 2011 - -@license: CeCILL (open-source compatible GPL) - cf. code source ou fichier LICENCE.txt joint - -@version: 0.01 - -@status: alpha -""" -#============================================================================== -__docformat__ = 'epytext en' - -__date__ = "2011-08-24" -__version__ = "0.01" - -#------------------------------------------------------------------------------ -# LICENCE pour le projet ExeFilter: - -# Copyright DGA/CELAR 2004-2008 -# Copyright NATO/NC3A 2008-2010 (PL changes after ExeFilter v1.1.0) -# Auteurs: -# - Philippe Lagadec (PL) - philippe.lagadec(a)laposte.net -# - Arnaud Kerréneur (AK) - arnaud.kerreneur(a)dga.defense.gouv.fr -# - Tanguy Vinceleux (TV) - tanguy.vinceleux(a)dga.defense.gouv.fr -# -# Ce logiciel est régi par la licence CeCILL soumise au droit français et -# respectant les principes de diffusion des logiciels libres. Vous pouvez -# utiliser, modifier et/ou redistribuer ce programme sous les conditions -# de la licence CeCILL telle que diffusée par le CEA, le CNRS et l'INRIA -# sur le site "http://www.cecill.info". Une copie de cette licence est jointe -# dans les fichiers Licence_CeCILL_V2-fr.html et Licence_CeCILL_V2-en.html. -# -# En contrepartie de l'accessibilité au code source et des droits de copie, -# de modification et de redistribution accordés par cette licence, il n'est -# offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, -# seule une responsabilité restreinte pèse sur l'auteur du programme, le -# titulaire des droits patrimoniaux et les concédants successifs. -# -# A cet égard l'attention de l'utilisateur est attirée sur les risques -# associés au chargement, à l'utilisation, à la modification et/ou au -# développement et à la reproduction du logiciel par l'utilisateur étant -# donné sa spécificité de logiciel libre, qui peut le rendre complexe à -# manipuler et qui le réserve donc à des développeurs et des professionnels -# avertis possédant des connaissances informatiques approfondies. Les -# utilisateurs sont donc invités à charger et tester l'adéquation du -# logiciel à leurs besoins dans des conditions permettant d'assurer la -# sécurité de leurs systèmes et ou de leurs données et, plus généralement, -# à l'utiliser et l'exploiter dans les mêmes conditions de sécurité. -# -# Le fait que vous puissiez accéder à cet en-tête signifie que vous avez -# pris connaissance de la licence CeCILL, et que vous en avez accepté les -# termes. - -#------------------------------------------------------------------------------ -# CHANGELOG: -# 2011-02-21 v0.01 PL: - 1st version -# 2011-08-24 PL: - code cleanup - -#------------------------------------------------------------------------------ -# TODO: -# + clean xml-stylesheet processing instruction (parameter to enable) - -#=== IMPORTS ================================================================== - -# modules standards Python: - -# XML parser - lxml or ElementTree: -try: - # lxml: best performance for XML processing - import lxml.etree as ET -except ImportError: - try: - # Python 2.5+: batteries included - import xml.etree.cElementTree as ET - except ImportError: - try: - # Python <2.5: standalone ElementTree install - import elementtree.cElementTree as ET - except ImportError: - raise ImportError, "lxml or ElementTree are not installed, "\ - +"see http://codespeak.net/lxml "\ - +"or http://effbot.org/zone/element-index.htm" - - -# modules du projet: -from commun import * -import Resultat, Conteneur -import Filtre -import thirdparty.RechercherRemplacer.RechercherRemplacer as RechercherRemplacer - - -#=== CONSTANTES =============================================================== - - -#=== CLASSES ================================================================== - - -class Filtre_XML (Filtre.Filtre): - """ - classe pour un filtre de fichiers XML. - - un objet Filtre sert à reconnaître le format d'un fichier et à - nettoyer le code éventuel qu'il contient. La classe Filtre_XML - correspond aux documents XML. - - @cvar nom: Le nom detaillé du filtre - @cvar nom_code: nom de code du filtre - @cvar extensions: liste des extensions de fichiers possibles - @cvar format_conteneur: indique si c'est un format conteneur - @cvar extractible: indique si il s'agit d'une archive - @cvar nettoyable: indique si il est possible de nettoyer avec ce filtre - @cvar date: date de la derniere modification du filtre - @cvar version: version du filtre - """ - - nom = _(u"Document XML") - extensions = [".xml"] - format_conteneur = False - extractible = False - nettoyable = True - - # date et version définies à partir de celles du module - date = __date__ - version = __version__ - - def __init__ (self, politique, parametres=None): - Filtre.Filtre.__init__(self, politique, parametres) - - def reconnait_format(self, fichier): - """ - analyse le format du fichier, et retourne True s'il correspond - au format recherché, False sinon. - """ - # Check if the XML file can be parsed without error: - self.tree = ET.parse(fichier.copie_temp()) - return True - - def nettoyer (self, fichier): - """ - Analyse et modifie le fichier pour supprimer tout code - exécutable qu'il peut contenir, si cela est possible. - Retourne un code résultat suivant l'action effectuée. - """ - # For now, let's just accept any XML without cleaning: - #return self.resultat_accepte(fichier) - return self.clean_simple_replace(fichier) - - def clean_simple_replace (self, fichier): - """ - Clean XML file using builtin simple replace method. - To be called from nettoyer() method. - Return Result object according to result. - Trigger an exception if an error occurs. - """ - motifs = [] - motifs.append( RechercherRemplacer.Motif(case_sensitive=False, - regex=r' ddeService(\s*)=(\s*)"[^_"]', remplacement=r' ddeService\1=\2"_')) - motifs.append( RechercherRemplacer.Motif(case_sensitive=False, - regex=r'(]*)?>\s*)DDE', remplacement=r'\1___')) - if len(motifs)>0: - # Create a temporary file - f_dest, chem_temp = newTempFile() - Journal.info2 (u"Temporary file: %s" % chem_temp) - # Open the source file - f_src = open(fichier.copie_temp(), 'rb') - Journal.info2 (u"Cleaning XML by replacing keywords") - n = RechercherRemplacer.rechercherRemplacer(motifs=motifs, - fich_src=f_src, fich_dest=f_dest, taille_identique=True, controle_apres=True) - f_src.close() - f_dest.close() - if n: - Journal.info2 (u"Tags in XML files have been disarmed.") - # File disarmed, replace original file - fichier.remplacer_copie_temp(chem_temp) - return self.resultat_nettoye(fichier) - else: - # No tag found - Journal.info2 (u"No XML tag disarmed.") - # Remove temporary file - os.remove(chem_temp) - return self.resultat_accepte(fichier) - else: - resultat = self.resultat_accepte(fichier) - return resultat - -#------------------------------------------------------------------------------ -# TESTS -#--------------------- -# tests de certaines fonctions si le module est lancé directement et non importé: -if __name__ == '__main__': - print "----------------------------------------------------------------------------" - print "Filtre XML v%s du %s - %s" % (__version__, __date__, __author__) - print "----------------------------------------------------------------------------" - print "" - #TODO: test XML parsing and cleaning +#!/usr/bin/python +# -*- coding: iso-8859-1 -*- +""" +Filtre_XML - ExeFilter + +Ce module contient la classe L{Filtre_XML.Filtre_XML} permettant de filtrer +les fichiers de type "document XML". + +Ce fichier fait partie du projet ExeFilter. +URL du projet: U{http://www.decalage.info/exefilter} + +@author: U{Philippe Lagadec} + +@contact: U{Philippe Lagadec} + +@copyright: Philippe Lagadec 2011 + +@license: CeCILL (open-source compatible GPL) + cf. code source ou fichier LICENCE.txt joint + +@version: 0.01 + +@status: alpha +""" +#============================================================================== +__docformat__ = 'epytext en' + +__date__ = "2011-08-24" +__version__ = "0.01" + +#------------------------------------------------------------------------------ +# LICENCE pour le projet ExeFilter: + +# Copyright DGA/CELAR 2004-2008 +# Copyright NATO/NC3A 2008-2010 (PL changes after ExeFilter v1.1.0) +# Auteurs: +# - Philippe Lagadec (PL) - philippe.lagadec(a)laposte.net +# - Arnaud Kerréneur (AK) - arnaud.kerreneur(a)dga.defense.gouv.fr +# - Tanguy Vinceleux (TV) - tanguy.vinceleux(a)dga.defense.gouv.fr +# +# Ce logiciel est régi par la licence CeCILL soumise au droit français et +# respectant les principes de diffusion des logiciels libres. Vous pouvez +# utiliser, modifier et/ou redistribuer ce programme sous les conditions +# de la licence CeCILL telle que diffusée par le CEA, le CNRS et l'INRIA +# sur le site "http://www.cecill.info". Une copie de cette licence est jointe +# dans les fichiers Licence_CeCILL_V2-fr.html et Licence_CeCILL_V2-en.html. +# +# En contrepartie de l'accessibilité au code source et des droits de copie, +# de modification et de redistribution accordés par cette licence, il n'est +# offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, +# seule une responsabilité restreinte pèse sur l'auteur du programme, le +# titulaire des droits patrimoniaux et les concédants successifs. +# +# A cet égard l'attention de l'utilisateur est attirée sur les risques +# associés au chargement, à l'utilisation, à la modification et/ou au +# développement et à la reproduction du logiciel par l'utilisateur étant +# donné sa spécificité de logiciel libre, qui peut le rendre complexe à +# manipuler et qui le réserve donc à des développeurs et des professionnels +# avertis possédant des connaissances informatiques approfondies. Les +# utilisateurs sont donc invités à charger et tester l'adéquation du +# logiciel à leurs besoins dans des conditions permettant d'assurer la +# sécurité de leurs systèmes et ou de leurs données et, plus généralement, +# à l'utiliser et l'exploiter dans les mêmes conditions de sécurité. +# +# Le fait que vous puissiez accéder à cet en-tête signifie que vous avez +# pris connaissance de la licence CeCILL, et que vous en avez accepté les +# termes. + +#------------------------------------------------------------------------------ +# CHANGELOG: +# 2011-02-21 v0.01 PL: - 1st version +# 2011-08-24 PL: - code cleanup + +#------------------------------------------------------------------------------ +# TODO: +# + clean xml-stylesheet processing instruction (parameter to enable) + +#=== IMPORTS ================================================================== + +# modules standards Python: + +# XML parser - lxml or ElementTree: +try: + # lxml: best performance for XML processing + import lxml.etree as ET +except ImportError: + try: + # Python 2.5+: batteries included + import xml.etree.cElementTree as ET + except ImportError: + try: + # Python <2.5: standalone ElementTree install + import elementtree.cElementTree as ET + except ImportError: + raise ImportError("lxml or ElementTree are not installed, "\ + +"see http://codespeak.net/lxml "\ + +"or http://effbot.org/zone/element-index.htm") + + +# modules du projet: +from commun import * +import Resultat, Conteneur +from . import Filtre +import thirdparty.RechercherRemplacer.RechercherRemplacer as RechercherRemplacer + + +#=== CONSTANTES =============================================================== + + +#=== CLASSES ================================================================== + + +class Filtre_XML (Filtre.Filtre): + """ + classe pour un filtre de fichiers XML. + + un objet Filtre sert à reconnaître le format d'un fichier et à + nettoyer le code éventuel qu'il contient. La classe Filtre_XML + correspond aux documents XML. + + @cvar nom: Le nom detaillé du filtre + @cvar nom_code: nom de code du filtre + @cvar extensions: liste des extensions de fichiers possibles + @cvar format_conteneur: indique si c'est un format conteneur + @cvar extractible: indique si il s'agit d'une archive + @cvar nettoyable: indique si il est possible de nettoyer avec ce filtre + @cvar date: date de la derniere modification du filtre + @cvar version: version du filtre + """ + + nom = _(u"Document XML") + extensions = [".xml"] + format_conteneur = False + extractible = False + nettoyable = True + + # date et version définies à partir de celles du module + date = __date__ + version = __version__ + + def __init__ (self, politique, parametres=None): + Filtre.Filtre.__init__(self, politique, parametres) + + def reconnait_format(self, fichier): + """ + analyse le format du fichier, et retourne True s'il correspond + au format recherché, False sinon. + """ + # Check if the XML file can be parsed without error: + self.tree = ET.parse(fichier.copie_temp()) + return True + + def nettoyer (self, fichier): + """ + Analyse et modifie le fichier pour supprimer tout code + exécutable qu'il peut contenir, si cela est possible. + Retourne un code résultat suivant l'action effectuée. + """ + # For now, let's just accept any XML without cleaning: + #return self.resultat_accepte(fichier) + return self.clean_simple_replace(fichier) + + def clean_simple_replace (self, fichier): + """ + Clean XML file using builtin simple replace method. + To be called from nettoyer() method. + Return Result object according to result. + Trigger an exception if an error occurs. + """ + motifs = [] + motifs.append( RechercherRemplacer.Motif(case_sensitive=False, + regex=r' ddeService(\s*)=(\s*)"[^_"]', remplacement=r' ddeService\1=\2"_')) + motifs.append( RechercherRemplacer.Motif(case_sensitive=False, + regex=r'(]*)?>\s*)DDE', remplacement=r'\1___')) + if len(motifs)>0: + # Create a temporary file + f_dest, chem_temp = newTempFile() + Journal.info2 (u"Temporary file: %s" % chem_temp) + # Open the source file + f_src = open(fichier.copie_temp(), 'rb') + Journal.info2 (u"Cleaning XML by replacing keywords") + n = RechercherRemplacer.rechercherRemplacer(motifs=motifs, + fich_src=f_src, fich_dest=f_dest, taille_identique=True, controle_apres=True) + f_src.close() + f_dest.close() + if n: + Journal.info2 (u"Tags in XML files have been disarmed.") + # File disarmed, replace original file + fichier.remplacer_copie_temp(chem_temp) + return self.resultat_nettoye(fichier) + else: + # No tag found + Journal.info2 (u"No XML tag disarmed.") + # Remove temporary file + os.remove(chem_temp) + return self.resultat_accepte(fichier) + else: + resultat = self.resultat_accepte(fichier) + return resultat + +#------------------------------------------------------------------------------ +# TESTS +#--------------------- +# tests de certaines fonctions si le module est lancé directement et non importé: +if __name__ == '__main__': + print("----------------------------------------------------------------------------") + print("Filtre XML v%s du %s - %s" % (__version__, __date__, __author__)) + print("----------------------------------------------------------------------------") + print("") + #TODO: test XML parsing and cleaning diff --git a/Filtres/Filtre_Zip.py b/Filtres/Filtre_Zip.py index ee88e47..33cf550 100644 --- a/Filtres/Filtre_Zip.py +++ b/Filtres/Filtre_Zip.py @@ -1,176 +1,176 @@ -#!/usr/bin/python -# -*- coding: iso-8859-1 -*- -""" -Filtre_ZIP - ExeFilter - -Ce module contient la classe L{Filtre_Zip.Filtre_Zip}, -pour filtrer les archives Zip. - -Ce fichier fait partie du projet ExeFilter. -URL du projet: U{http://www.decalage.info/exefilter} - -@organization: DGA/CELAR -@author: U{Philippe Lagadec} -@author: U{Arnaud Kerréneur} -@author: U{Tanguy Vinceleux} - -@contact: U{Philippe Lagadec} - -@copyright: DGA/CELAR 2004-2008 -@copyright: NATO/NC3A 2008 (modifications PL apres v1.1.0) - -@license: CeCILL (open-source compatible GPL) - cf. code source ou fichier LICENCE.txt joint - -@version: 1.03 - -@status: beta -""" -#============================================================================== -__docformat__ = 'epytext en' - -__date__ = "2008-04-20" -__version__ = "1.03" - -#------------------------------------------------------------------------------ -# LICENCE pour le projet ExeFilter: - -# Copyright DGA/CELAR 2004-2008 -# Copyright NATO/NC3A 2008 (PL changes after v1.1.0) -# Auteurs: -# - Philippe Lagadec (PL) - philippe.lagadec(a)laposte.net -# - Arnaud Kerréneur (AK) - arnaud.kerreneur(a)dga.defense.gouv.fr -# - Tanguy Vinceleux (TV) - tanguy.vinceleux(a)dga.defense.gouv.fr -# -# Ce logiciel est régi par la licence CeCILL soumise au droit français et -# respectant les principes de diffusion des logiciels libres. Vous pouvez -# utiliser, modifier et/ou redistribuer ce programme sous les conditions -# de la licence CeCILL telle que diffusée par le CEA, le CNRS et l'INRIA -# sur le site "http://www.cecill.info". Une copie de cette licence est jointe -# dans les fichiers Licence_CeCILL_V2-fr.html et Licence_CeCILL_V2-en.html. -# -# En contrepartie de l'accessibilité au code source et des droits de copie, -# de modification et de redistribution accordés par cette licence, il n'est -# offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, -# seule une responsabilité restreinte pèse sur l'auteur du programme, le -# titulaire des droits patrimoniaux et les concédants successifs. -# -# A cet égard l'attention de l'utilisateur est attirée sur les risques -# associés au chargement, à l'utilisation, à la modification et/ou au -# développement et à la reproduction du logiciel par l'utilisateur étant -# donné sa spécificité de logiciel libre, qui peut le rendre complexe à -# manipuler et qui le réserve donc à des développeurs et des professionnels -# avertis possédant des connaissances informatiques approfondies. Les -# utilisateurs sont donc invités à charger et tester l'adéquation du -# logiciel à leurs besoins dans des conditions permettant d'assurer la -# sécurité de leurs systèmes et ou de leurs données et, plus généralement, -# à l'utiliser et l'exploiter dans les mêmes conditions de sécurité. -# -# Le fait que vous puissiez accéder à cet en-tête signifie que vous avez -# pris connaissance de la licence CeCILL, et que vous en avez accepté les -# termes. - -#------------------------------------------------------------------------------ -# HISTORIQUE: -# 24/10/2004 v0.01 PL: - 1ère version -# 2004-2005 PL,AK: - evolutions -# 12/01/2007 v1.00 PL: - version 1.00 officielle -# 2008-02-25 v1.01 PL: - licence CeCILL -# 2008-03-24 v1.02 PL: - ajout de _() pour traduction gettext des chaines -# - simplification dans nettoyer() en appelant resultat_* -# 2008-04-20 v1.03 PL: - ajout politique dans appel Conteneur_Zip - -#------------------------------------------------------------------------------ -# A FAIRE: - -#------------------------------------------------------------------------------ - -#=== IMPORTS ================================================================== - -# modules standards Python: -import zipfile, sys - -# modules du projet: -from commun import * -import Filtre, Conteneur_Zip, Resultat - -#=== CONSTANTES =============================================================== - - -#=== CLASSES ================================================================== - -#------------------------------------------------------------------------------ -# class FILTRE_ZIP -#--------------------- -class Filtre_Zip (Filtre.Filtre): - """ - classe pour un filtre de fichiers Zip. - - un objet Filtre sert à reconnaître le format d'un fichier et à - nettoyer le code éventuel qu'il contient. La classe Filtre_Zip - correspond aux archives compressées Zip. - - - @cvar nom: Le nom detaillé du filtre - @cvar nom_code: nom de code du filtre - @cvar extensions: liste des extensions de fichiers possibles - @cvar format_conteneur: indique si c'est un format conteneur - @cvar extractible: indique si il s'agit d'une archive - @cvar nettoyable: indique si il est possible de nettoyer avec ce filtre - @cvar date: date de la derniere modification du filtre - @cvar version: version du filtre - """ - - nom = _(u"Archive Zip") - extensions = [".zip"] - format_conteneur = True - extractible = True - nettoyable = False - - # date et version définies à partir de celles du module - date = __date__ - version = __version__ - - def reconnait_format(self, fichier): - """Analyse le format du fichier, et retourne True s'il correspond - au format recherché, False sinon. - - @param fichier: fichier à analyser - @type fichier: objet L{Fichier.Fichier} - """ - Journal.debug('Verif format Zip pour "%s"' % fichier.nom) - return zipfile.is_zipfile(str_lat1(fichier.copie_temp())) - - def nettoyer (self, fichier): - """Analyse et modifie le fichier pour supprimer tout code - exécutable qu'il peut contenir, si cela est possible. - Retourne un code résultat suivant l'action effectuée. - - @param fichier: fichier à nettoyer - @type fichier: objet L{Fichier} - - @return: résultat du nettoyage - @rtype : objet L{Resultat} - """ - # ce format ne contient jamais de code en soi, mais comme c'est - # un conteneur, il faut nettoyer tous les fichiers qu'il contient: - try: - conteneur_zip = Conteneur_Zip.Conteneur_Zip (fichier.copie_temp(), - "", fichier, politique=self.politique) - except zipfile.BadZipfile: - # si on obtient cette exception, c'est que le module zipfile ne - # supporte pas le format de ce fichier zip. - erreur = str(sys.exc_info()[1]) - return self.resultat_format_incorrect(fichier, erreur) - liste_resultats = conteneur_zip.nettoyer(self.politique) - # on crée un objet Resultat par défaut pour le zip - resultat_zip = Resultat.Resultat(fichier = fichier) - # puis on fusionne tous les résultats des fichiers inclus - resultat_zip.ajouter_conteneur(liste_resultats, self.nom) - return resultat_zip - - def conteneur (self, fichier_conteneur, destination, fichier): - return Conteneur_Zip.Conteneur_Zip (fichier_conteneur, - destination, fichier) - - +#!/usr/bin/python +# -*- coding: iso-8859-1 -*- +""" +Filtre_ZIP - ExeFilter + +Ce module contient la classe L{Filtre_Zip.Filtre_Zip}, +pour filtrer les archives Zip. + +Ce fichier fait partie du projet ExeFilter. +URL du projet: U{http://www.decalage.info/exefilter} + +@organization: DGA/CELAR +@author: U{Philippe Lagadec} +@author: U{Arnaud Kerréneur} +@author: U{Tanguy Vinceleux} + +@contact: U{Philippe Lagadec} + +@copyright: DGA/CELAR 2004-2008 +@copyright: NATO/NC3A 2008 (modifications PL apres v1.1.0) + +@license: CeCILL (open-source compatible GPL) + cf. code source ou fichier LICENCE.txt joint + +@version: 1.03 + +@status: beta +""" +#============================================================================== +__docformat__ = 'epytext en' + +__date__ = "2008-04-20" +__version__ = "1.03" + +#------------------------------------------------------------------------------ +# LICENCE pour le projet ExeFilter: + +# Copyright DGA/CELAR 2004-2008 +# Copyright NATO/NC3A 2008 (PL changes after v1.1.0) +# Auteurs: +# - Philippe Lagadec (PL) - philippe.lagadec(a)laposte.net +# - Arnaud Kerréneur (AK) - arnaud.kerreneur(a)dga.defense.gouv.fr +# - Tanguy Vinceleux (TV) - tanguy.vinceleux(a)dga.defense.gouv.fr +# +# Ce logiciel est régi par la licence CeCILL soumise au droit français et +# respectant les principes de diffusion des logiciels libres. Vous pouvez +# utiliser, modifier et/ou redistribuer ce programme sous les conditions +# de la licence CeCILL telle que diffusée par le CEA, le CNRS et l'INRIA +# sur le site "http://www.cecill.info". Une copie de cette licence est jointe +# dans les fichiers Licence_CeCILL_V2-fr.html et Licence_CeCILL_V2-en.html. +# +# En contrepartie de l'accessibilité au code source et des droits de copie, +# de modification et de redistribution accordés par cette licence, il n'est +# offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, +# seule une responsabilité restreinte pèse sur l'auteur du programme, le +# titulaire des droits patrimoniaux et les concédants successifs. +# +# A cet égard l'attention de l'utilisateur est attirée sur les risques +# associés au chargement, à l'utilisation, à la modification et/ou au +# développement et à la reproduction du logiciel par l'utilisateur étant +# donné sa spécificité de logiciel libre, qui peut le rendre complexe à +# manipuler et qui le réserve donc à des développeurs et des professionnels +# avertis possédant des connaissances informatiques approfondies. Les +# utilisateurs sont donc invités à charger et tester l'adéquation du +# logiciel à leurs besoins dans des conditions permettant d'assurer la +# sécurité de leurs systèmes et ou de leurs données et, plus généralement, +# à l'utiliser et l'exploiter dans les mêmes conditions de sécurité. +# +# Le fait que vous puissiez accéder à cet en-tête signifie que vous avez +# pris connaissance de la licence CeCILL, et que vous en avez accepté les +# termes. + +#------------------------------------------------------------------------------ +# HISTORIQUE: +# 24/10/2004 v0.01 PL: - 1ère version +# 2004-2005 PL,AK: - evolutions +# 12/01/2007 v1.00 PL: - version 1.00 officielle +# 2008-02-25 v1.01 PL: - licence CeCILL +# 2008-03-24 v1.02 PL: - ajout de _() pour traduction gettext des chaines +# - simplification dans nettoyer() en appelant resultat_* +# 2008-04-20 v1.03 PL: - ajout politique dans appel Conteneur_Zip + +#------------------------------------------------------------------------------ +# A FAIRE: + +#------------------------------------------------------------------------------ + +#=== IMPORTS ================================================================== + +# modules standards Python: +import zipfile, sys + +# modules du projet: +from commun import * +import Conteneur_Zip, Resultat +from . import Filtre +#=== CONSTANTES =============================================================== + + +#=== CLASSES ================================================================== + +#------------------------------------------------------------------------------ +# class FILTRE_ZIP +#--------------------- +class Filtre_Zip (Filtre.Filtre): + """ + classe pour un filtre de fichiers Zip. + + un objet Filtre sert à reconnaître le format d'un fichier et à + nettoyer le code éventuel qu'il contient. La classe Filtre_Zip + correspond aux archives compressées Zip. + + + @cvar nom: Le nom detaillé du filtre + @cvar nom_code: nom de code du filtre + @cvar extensions: liste des extensions de fichiers possibles + @cvar format_conteneur: indique si c'est un format conteneur + @cvar extractible: indique si il s'agit d'une archive + @cvar nettoyable: indique si il est possible de nettoyer avec ce filtre + @cvar date: date de la derniere modification du filtre + @cvar version: version du filtre + """ + + nom = _(u"Archive Zip") + extensions = [".zip"] + format_conteneur = True + extractible = True + nettoyable = False + + # date et version définies à partir de celles du module + date = __date__ + version = __version__ + + def reconnait_format(self, fichier): + """Analyse le format du fichier, et retourne True s'il correspond + au format recherché, False sinon. + + @param fichier: fichier à analyser + @type fichier: objet L{Fichier.Fichier} + """ + Journal.debug('Verif format Zip pour "%s"' % fichier.nom) + return zipfile.is_zipfile(str_lat1(fichier.copie_temp())) + + def nettoyer (self, fichier): + """Analyse et modifie le fichier pour supprimer tout code + exécutable qu'il peut contenir, si cela est possible. + Retourne un code résultat suivant l'action effectuée. + + @param fichier: fichier à nettoyer + @type fichier: objet L{Fichier} + + @return: résultat du nettoyage + @rtype : objet L{Resultat} + """ + # ce format ne contient jamais de code en soi, mais comme c'est + # un conteneur, il faut nettoyer tous les fichiers qu'il contient: + try: + conteneur_zip = Conteneur_Zip.Conteneur_Zip (fichier.copie_temp(), + "", fichier, politique=self.politique) + except zipfile.BadZipfile: + # si on obtient cette exception, c'est que le module zipfile ne + # supporte pas le format de ce fichier zip. + erreur = str(sys.exc_info()[1]) + return self.resultat_format_incorrect(fichier, erreur) + liste_resultats = conteneur_zip.nettoyer(self.politique) + # on crée un objet Resultat par défaut pour le zip + resultat_zip = Resultat.Resultat(fichier = fichier) + # puis on fusionne tous les résultats des fichiers inclus + resultat_zip.ajouter_conteneur(liste_resultats, self.nom) + return resultat_zip + + def conteneur (self, fichier_conteneur, destination, fichier): + return Conteneur_Zip.Conteneur_Zip (fichier_conteneur, + destination, fichier) + + diff --git a/Filtres/__init__.py b/Filtres/__init__.py index c0539f2..29eafc9 100644 --- a/Filtres/__init__.py +++ b/Filtres/__init__.py @@ -149,7 +149,7 @@ def liste_filtres(): fichier_module = fichier[:len] module = "Filtres." + fichier_module # on vérifie si le module a déjà été chargé - if __liste_filtres__.has_key(module) : continue + if __liste_filtres__ in module : continue # si le module n'a pas encore été chargé : on l'importe __liste_filtres__[module] = {} __import__(module) @@ -164,26 +164,26 @@ def liste_filtres(): __liste_filtres__[module][module + "." + el] = {} nom = extensions = version = date = "" # charge le nom du filtre - if sys.modules[module].__dict__[el].__dict__.has_key("nom") : + if sys.modules[module].__dict__[el].__dict__ in "nom" : __liste_filtres__[module][module + "." + el]["nom"] = \ sys.modules[module].__dict__[el].__dict__["nom"] else: __liste_filtres__[module][module + "." + el]["nom"] = nom #on charge les extensions gérées par le filtre - if sys.modules[module].__dict__[el].__dict__.has_key("extensions") : + if sys.modules[module].__dict__[el].__dict__ in "extensions" : __liste_filtres__[module][module + "." + el]["extensions"] = \ ', '.join(sys.modules[module].__dict__[el].__dict__["extensions"]) else: __liste_filtres__[module][module + "." + el]["extensions"] = extensions # on charge la version du filtre - if sys.modules[module].__dict__[el].__dict__.has_key("version") : + if sys.modules[module].__dict__[el].__dict__ in "version" : __liste_filtres__[module][module + "." + el]["version"] = \ sys.modules[module].__dict__[el].__dict__["version"] else: __liste_filtres__[module][module + "." + el]["version"] = version # on charge la date de mise à jour du filtre - if sys.modules[module].__dict__[el].__dict__.has_key("date") : + if sys.modules[module].__dict__[el].__dict__ in "date" : __liste_filtres__[module][module + "." + el]["date"] = \ sys.modules[module].__dict__[el].__dict__["date"] else: @@ -251,7 +251,7 @@ def classes_filtres(): modules_importes.append(nom_module) __import__(nom_module) # ensuite on parcourt les objets du module: - for nom_objet, objet in sys.modules[nom_module].__dict__.iteritems(): + for nom_objet, objet in sys.modules[nom_module].__dict__.items(): # si l'objet commence par "Filtre_" et est une classe if nom_objet.startswith("Filtre_") and inspect.isclass(objet): # ...et si c'est une sous-classe de Filtre diff --git a/Journal.py b/Journal.py index 89771b3..31d4a05 100644 --- a/Journal.py +++ b/Journal.py @@ -1,673 +1,673 @@ -#!/usr/bin/python -# -*- coding: iso-8859-1 -*- -""" -Journal - ExeFilter - -Module qui prend en charge la journalisation et l'affichage des événements. - -Les fonctions du module permettent de journaliser les évènements à l'écran sur -la console, dans un fichier texte et/ou un serveur syslog via UDP. - -Ce fichier fait partie du projet ExeFilter. -URL du projet: U{http://www.decalage.info/exefilter} - -@organization: DGA/CELAR -@author: U{Philippe Lagadec} -@author: U{Arnaud Kerréneur} -@author: U{Tanguy Vinceleux} - -@contact: U{Philippe Lagadec} - -@copyright: DGA/CELAR 2004-2008 -@copyright: NATO/NC3A 2008-2010 (modifications PL apres ExeFilter v1.1.0) - -@license: CeCILL (open-source compatible GPL) - cf. code source ou fichier LICENCE.txt joint - see LICENCE.txt - -@version: 1.04 - -@status: beta -""" - -#============================================================================== -__docformat__ = 'epytext en' - -#__author__ = "Philippe Lagadec, Tanguy Vinceleux, Arnaud Kerréneur (DGA/CELAR)" -__date__ = "2010-12-03" -__version__ = "1.05" - -#------------------------------------------------------------------------------ -# LICENCE pour le projet ExeFilter: - -# Copyright DGA/CELAR 2004-2008 -# Copyright NATO/NC3A 2008-2010 (modifications PL apres ExeFilter v1.1.0) -# Auteurs: -# - Philippe Lagadec (PL) - philippe.lagadec(a)laposte.net -# - Arnaud Kerréneur (AK) - arnaud.kerreneur(a)dga.defense.gouv.fr -# - Tanguy Vinceleux (TV) - tanguy.vinceleux(a)dga.defense.gouv.fr -# -# Ce logiciel est régi par la licence CeCILL soumise au droit français et -# respectant les principes de diffusion des logiciels libres. Vous pouvez -# utiliser, modifier et/ou redistribuer ce programme sous les conditions -# de la licence CeCILL telle que diffusée par le CEA, le CNRS et l'INRIA -# sur le site "http://www.cecill.info". Une copie de cette licence est jointe -# dans les fichiers Licence_CeCILL_V2-fr.html et Licence_CeCILL_V2-en.html. -# -# En contrepartie de l'accessibilité au code source et des droits de copie, -# de modification et de redistribution accordés par cette licence, il n'est -# offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, -# seule une responsabilité restreinte pèse sur l'auteur du programme, le -# titulaire des droits patrimoniaux et les concédants successifs. -# -# A cet égard l'attention de l'utilisateur est attirée sur les risques -# associés au chargement, à l'utilisation, à la modification et/ou au -# développement et à la reproduction du logiciel par l'utilisateur étant -# donné sa spécificité de logiciel libre, qui peut le rendre complexe à -# manipuler et qui le réserve donc à des développeurs et des professionnels -# avertis possédant des connaissances informatiques approfondies. Les -# utilisateurs sont donc invités à charger et tester l'adéquation du -# logiciel à leurs besoins dans des conditions permettant d'assurer la -# sécurité de leurs systèmes et ou de leurs données et, plus généralement, -# à l'utiliser et l'exploiter dans les mêmes conditions de sécurité. -# -# Le fait que vous puissiez accéder à cet en-tête signifie que vous avez -# pris connaissance de la licence CeCILL, et que vous en avez accepté les -# termes. - -#------------------------------------------------------------------------------ -# HISTORIQUE: -# 19/01/2005 v0.01 AK: - 1ère version -# 2005-2007 PL,AK: - nombreuses évolutions + contributions de Y. Bidan -# et C. Catherin -# 12/01/2007 v1.00 PL: - version 1.00 officielle -# 2007-09-20 v1.01 PL: - licence CeCILL -# 2010-02-07 v1.02 PL: - removed path module import -# 2010-02-23 v1.03 PL: - updated plx import -# 2010-11-26 v1.04 PL: - code cleanup, use ExtendedLogger class -# 2010-12-03 v1.05 PL: - append to log files instead of overwriting -# - separate function init_console_logging - -#------------------------------------------------------------------------------ -# TODO: -# + split init_journal into several functions: -# - init_console to setup console logging at the root level only once -# - set_logfile(s) to setup logfile(s) for each session, at the logger level -# - set_syslog to setup syslog the same way -# + when used as a library, xf should only have a NullHandler -# + fix encoding issue on Windows console -# + option to use a rotating file handler to limit log file size -# ? each new call to init_journal should use a different name for the logger -# (to support multithreading) -# + do not use a global logger variable, but a per-session logger object -# + init_jounal: return an ExtendedLogger object -# + remove obsolete module-level functions, once other modules are fixed -# - corriger bug syslog: messages dupliqués au 2ème transfert d'affilée depuis -# l'IHM (puis retour à la normale au 3ème transfert) -# - corriger les docstrings pour éviter les warnings epydoc -# - issue when ExeFilter is embedded into another application that uses logging: -# it should be possible to create a logger object without handlers. -# (handlers should only be defined by the main app to avoid duplication) -# ? rename "INFO2" level to "DETAIL" or "TECH" to be clearer? - -# EVOLUTIONS ENVISAGEES: -# - avec Python 2.4.2, Formatter_Latin1 n'est plus nécessaire, utiliser le -# nouveau paramètre encoding de FileHandler -#------------------------------------------------------------------------------ - - -#=== IMPORTS ================================================================== - -import logging, logging.handlers, os.path, sys - -# modules du projet: -from commun import * -import commun -from thirdparty.plx.plx import * - - -#=== CONSTANTES =============================================================== - -# OBSOLETE: -SYSLOG = 0 -FICHIER = 1 -SYS_FIC = 2 - -# niveaux de jouralisation ajoutés aux niveaux du module standard logging: -# (cf. notes de développements pour plus de détails) -##CRITICAL = 50 # Erreur fatale, le processus ne peut continuer -##ERROR = 40 # Erreur grave mais non fatale -IMPORTANT = 35 # Information importante à toujours journaliser, mais qui - # n'est pas un warning. -##WARNING = 30 # Avertissement, problème potentiel -##INFO = 20 # Information normale -INFO2 = 15 # Information technique -##DEBUG = 10 # Information de déboguage pour développeur -##NOTSET = 0 - -#=== VARIABLES GLOBALES ======================================================= - -# flag pour éviter de faire 2 init_journal() -init_effectue = False - -# global logger object -logger = None - -#=== CLASSES ================================================================== - -#------------------------------------------------------------------------------ -# classe Formatter_console -#-------------------------- - -class Formatter_console (logging.Formatter): - """classe qui hérite de logging.Formatter et ajoute une conversion - grâce au codec adapté à l'OS pour un affichage correct sur la - console. (CMD sous Windows) - """ - - def format(self, record): - # on appelle d'abord le format() d'origine pour obtenir la chaîne: - chaine = logging.Formatter.format(self, record) - # ensuite on convertit le résultat si besoin: - return str_console(chaine, errors='replace') - -#------------------------------------------------------------------------------ -# classe Formatter_Latin1 -#------------------------- - -class Formatter_Latin1 (logging.Formatter): - """classe qui hérite de logging.Formatter et ajoute une conversion - grâce au codec latin_1 pour les messages Unicode. - (Sinon par défaut Python emploie UTF-8) - """ - - def format(self, record): - # on appelle d'abord le format() d'origine pour obtenir la chaîne: - chaine = logging.Formatter.format(self, record) - # ensuite on convertit le résultat en str si c'est de l'Unicode: - return str_lat1(chaine, errors='replace') - - -class ExtendedLogger (logging.Logger): - """ - extended Logger class for ExeFilter, based on logging.Logger: - - with additional methods to support more logging levels - - with conversion from strings to Unicode if needed - """ - - def debug(self, msg, *args, **kwargs): - """Pour journaliser un évènement de niveau DEBUG, pour le journal de - débogage technique en mode DEBUG pour le développement uniquement. - - @param msg: le message de l'évènement à journaliser - @type msg: str, unicode, objet quelconque - - @param exc_info: quand ce paramètre vaut True ou 1, l'exception en cours est - journalisée avec le message. - @type exc_info: bool, int, str, ... - """ - # ci-dessous la virgule est indispensable sinon (unistr(chaine)) est une - # chaîne et non un tuple... - args = (unistr(msg),) + args - # syntaxe bizarre: "extended call syntax" --> cf. "apply" dans aide Python - logging.Logger.debug(self, *args, **kwargs) - - - def info2(self, msg, *args, **kwargs): - """Pour journaliser un évènement de niveau INFO2, pour le journal de - débogage technique. - - @param msg: le message de l'évènement à journaliser - @type msg: str, unicode, objet quelconque - - @param exc_info: quand ce paramètre vaut True ou 1, l'exception en cours est - journalisée avec le message. - @type exc_info: bool, int, str, ... - """ - # syntaxe bizarre: "extended call syntax" --> cf. "apply" dans aide Python - args = (INFO2, unistr(msg)) + args - logging.Logger.log(self, *args, **kwargs) - - - def info(self, msg, *args, **kwargs): - """Pour journaliser un évènement de niveau INFO, qui sera affiché sur la - console et enregistré dans différents journaux. - Exemple: un fichier nettoyé ou accepté. - - @param msg: le message de l'évènement à journaliser - @type msg: str, unicode, objet quelconque - - @param exc_info: quand ce paramètre vaut True ou 1, l'exception en cours est - journalisée avec le message. - @type exc_info: bool, int, str, ... - """ - # syntaxe bizarre: "extended call syntax" --> cf. "apply" dans aide Python - args = (unistr(msg),) + args - logging.Logger.info(self, *args, **kwargs) - - - def warning(self, msg, *args, **kwargs): - """Pour journaliser un évènement de niveau WARNING, qui sera affiché sur la - console et enregistré dans différents journaux. - Exemple: un fichier refusé. - - @param msg: le message de l'évènement à journaliser - @type msg: str, unicode, objet quelconque - - @param exc_info: quand ce paramètre vaut True ou 1, l'exception en cours est - journalisée avec le message. - @type exc_info: bool, int, str, ... - """ - # syntaxe bizarre: "extended call syntax" --> cf. "apply" dans aide Python - args = (unistr(msg),) + args - logging.Logger.warning(self, *args, **kwargs) - - - def important(self, msg, *args, **kwargs): - """Pour journaliser un évènement de niveau IMPORTANT (entre WARNING ET ERROR). - Exemple: le début et la fin d'un transfert. - - @param msg: le message de l'évènement à journaliser - @type msg: str, unicode, objet quelconque - - @param exc_info: quand ce paramètre vaut True ou 1, l'exception en cours est - journalisée avec le message. - @type exc_info: bool, int, str, ... - """ - # syntaxe bizarre: "extended call syntax" --> cf. "apply" dans aide Python - args = (IMPORTANT, unistr(msg)) + args - logging.Logger.log(self, *args, **kwargs) - - - def error(self, msg, *args, **kwargs): - """Pour journaliser un évènement de niveau ERROR, qui sera affiché sur la - console et enregistré dans différents journaux. - Exemple: une erreur anormale, qui ne nécessite pas l'arrêt du processus. - - @param msg: le message de l'évènement à journaliser - @type msg: str, unicode, objet quelconque - - @param exc_info: quand ce paramètre vaut True ou 1, l'exception en cours est - journalisée avec le message. - @type exc_info: bool, int, str, ... - """ - # syntaxe bizarre: "extended call syntax" --> cf. "apply" dans aide Python - args = (unistr(msg),) + args - logging.Logger.error(self, *args, **kwargs) - - - def exception(self, msg, *args, **kwargs): - """Pour journaliser un évènement de niveau ERROR avec le texte complet de - l'exception en cours, qui sera affiché sur la console et enregistré dans - différents journaux. Cette fonction doit normalement être appelée dans un - bloc "except". - Exemple: une erreur anormale, qui ne nécessite pas l'arrêt du processus. - - @param msg: le message de l'évènement à journaliser - @type msg: str, unicode, objet quelconque - """ - args = (unistr(msg),) + args - logging.Logger.exception(self, *args, **kwargs) - - - def critical(self, msg, *args, **kwargs): - """Pour journaliser un évènement de niveau CRITICAL, qui sera affiché sur la - console et enregistré dans différents journaux. - Exemple: une erreur critique, qui nécessite l'arrêt du processus. - - @param msg: le message de l'évènement à journaliser - @type msg: str, unicode, objet quelconque - - @param exc_info: quand ce paramètre vaut True ou 1, l'exception en cours est - journalisée avec le message. - @type exc_info: bool, int, str, ... - """ - args = (unistr(msg),) + args - logging.Logger.critical(self, *args, **kwargs) - - -#=== INITIALISATION =========================================================== - -# set ExtendedLogger class as default instead of logging.Logger: -logging.setLoggerClass(ExtendedLogger) -# set global logger -logger = logging.getLogger('ExeFilter') - -# two additional logging levels: -logging.addLevelName (INFO2, "INFO2") -logging.addLevelName (IMPORTANT, "IMPORTANT") - - - -#=== FONCTIONS ================================================================ - -def init_console_logging(): - """ - initialize a console handler for the root logger, so that messages are - displayed on the console. - Should only be called if the main application does not already setup logging - handlers. - Should be called early, before any call to logging. - """ - # get the root logger - rootLogger = logging.getLogger('') - # make sure the root logger accepts up to debug messages: - rootLogger.setLevel(logging.DEBUG) - - # 1) on ajoute un affichage console de niveau INFO, - # qui n'affiche que les messages sans date/niveau: - log_console = logging.StreamHandler(sys.stdout) - log_console.setLevel(logging.INFO) - # si on est en mode debug, niveau=DEBUG: - if commun.mode_debug(): - log_console.setLevel(logging.DEBUG) - log_console.setFormatter(Formatter_console('%(message)s')) - rootLogger.addHandler(log_console) - - -#------------------------------------------------------------------------------ -# init_journal -#------------------- - -def init_journal (politique, journal_secu=None, journal_debug=None): - """ - Initialise the logging system for a session. Handlers to use log files and - syslog are enabled for the logger object according to the policy. - - - politique: policy object - - journal_secu: filename/path of the security log file (INFO level) - - journal_debug: filename/path of the debug log file (INFO2 level) - - Note: the policy controls if log files are enabled or not - """ -## # on récupère le Logger racine du module logging: -## rootLogger = logging.getLogger('') - # make sure the logger object accepts up to debug messages: - logger.setLevel(logging.DEBUG) - - # on supprime tout handler qui serait déjà configuré, pour éviter les - # problèmes éventuels (ça arrive): - for h in logger.handlers: - try: - h.flush() - except: - pass - h.close() - logger.removeHandler(h) - - # 2) on ajoute une sortie vers un fichier de niveau INFO2, - # qui affiche les messages avec date/niveau: - if politique.parametres['journal_debug'].valeur: - # append to existing log file instead of replacing it ('w' mode): - log_fichier = logging.FileHandler(journal_debug) #, 'w') - log_fichier.setLevel(INFO2) - # si on est en mode debug, niveau=DEBUG: - if commun.mode_debug(): - log_fichier.setLevel(logging.DEBUG) - log_fichier.setFormatter(Formatter_Latin1( - #fmt = '%(asctime)s %(name)-12s %(levelname)-9s %(message)s', - fmt = '%(asctime)s %(levelname)-9s %(message)s', - datefmt = '%d/%m/%y %H:%M')) - logger.addHandler(log_fichier) - - # 3) on ajoute une sortie vers un fichier (journal de sécurité) de niveau INFO, - # qui affiche les messages avec date/niveau: - if politique.parametres['journal_securite'].valeur: - # append to existing log file instead of replacing it ('w' mode): - log_fichier2 = logging.FileHandler(journal_secu) #, 'w') - log_fichier2.setLevel(logging.INFO) - log_fichier2.setFormatter(Formatter_Latin1( - #fmt = '%(asctime)s %(name)-12s %(levelname)-9s %(message)s', - fmt = '%(asctime)s %(levelname)-9s %(message)s', - datefmt = '%d/%m/%y %H:%M')) - logger.addHandler(log_fichier2) - - # 4) on ajoute une sortie vers un serveur syslog de niveau WARNING, - # qui affiche les messages avec date/niveau: - # On doit d'abord corriger le tableau de conversion des priorités logging/syslog - # (sinon levée d'exceptions pour les niveaux que nous avons ajoutés...) - # notre niveau INFO correspond au niveau NOTICE de syslog: - if politique.parametres['journal_syslog'].valeur: - logging.handlers.SysLogHandler.priority_names['info'] = logging.handlers.SysLogHandler.LOG_NOTICE - # notre niveau INFO2 correspond au niveau INFO de syslog: - logging.handlers.SysLogHandler.priority_names['info2'] = logging.handlers.SysLogHandler.LOG_INFO - # notre niveau IMPORTANT correspond au niveau WARNING de syslog: - logging.handlers.SysLogHandler.priority_names['important'] = logging.handlers.SysLogHandler.LOG_WARNING - # ensuite on peut créer notre instance de SysLogHandler: - adresse_ip = politique.parametres['serveur_syslog'].valeur - port_udp = politique.parametres['port_syslog'].valeur - log_syslog = logging.handlers.SysLogHandler((adresse_ip, port_udp)) - log_syslog.setLevel(logging.WARNING) - #log_syslog.setFormatter(Formatter_Latin1( - # fmt = '%(asctime)s %(name)-12s %(levelname)-9s %(message)s', - # datefmt = '%d/%m/%y %H:%M')) - # syslog n'a pas besoin des infos date/heure et niveau, juste le message: - log_syslog.setFormatter(Formatter_Latin1('%(message)s')) - logger.addHandler(log_syslog) - - # return the logger object (in the future there will be one per session): - return logger - - -#------------------------------------------------------------------------------ -# fermer_journal -#------------------- - -def fermer_journal() : - """ - Ferme proprement l'objet journal - """ - try : - # on récupère le Logger racine du module logging: -## rootLogger = logging.getLogger('') - # on ferme proprement chaque handler: - for h in logger.handlers: - h.flush() - h.close() - logger.removeHandler(h) -## log_console.flush() -## log_console.close() -## log_fichier.flush() -## log_fichier.close() -## log_fichier2.flush() -## log_fichier2.close() -## log_syslog.flush() -## log_syslog.close() -## # puis on les supprime: -## logger.removeHandler(log_console) -## logger.removeHandler(log_fichier) -## logger.removeHandler(log_fichier2) -## logger.removeHandler(log_syslog) - except: - raise - - -#------------------------------------------------------------------------------ -# JOURNALISATION -#------------------- -# cf. aide du module logging - -def debug(msg, *args, **kwargs): - """Pour journaliser un évènement de niveau DEBUG, pour le journal de - débogage technique en mode DEBUG pour le développement uniquement. - - @param msg: le message de l'évènement à journaliser - @type msg: str, unicode, objet quelconque - - @param exc_info: quand ce paramètre vaut True ou 1, l'exception en cours est - journalisée avec le message. - @type exc_info: bool, int, str, ... - """ - # ci-dessous la virgule est indispensable sinon (unistr(chaine)) est une - # chaîne et non un tuple... - args = (unistr(msg),) + args - # syntaxe bizarre: "extended call syntax" --> cf. "apply" dans aide Python - logger.debug(*args, **kwargs) - -def info2(msg, *args, **kwargs): - """Pour journaliser un évènement de niveau INFO2, pour le journal de - débogage technique. - - @param msg: le message de l'évènement à journaliser - @type msg: str, unicode, objet quelconque - - @param exc_info: quand ce paramètre vaut True ou 1, l'exception en cours est - journalisée avec le message. - @type exc_info: bool, int, str, ... - """ - # syntaxe bizarre: "extended call syntax" --> cf. "apply" dans aide Python - args = (INFO2, unistr(msg)) + args - logger.log(*args, **kwargs) - -def info(msg, *args, **kwargs): - """Pour journaliser un évènement de niveau INFO, qui sera affiché sur la - console et enregistré dans différents journaux. - Exemple: un fichier nettoyé ou accepté. - - @param msg: le message de l'évènement à journaliser - @type msg: str, unicode, objet quelconque - - @param exc_info: quand ce paramètre vaut True ou 1, l'exception en cours est - journalisée avec le message. - @type exc_info: bool, int, str, ... - """ - # syntaxe bizarre: "extended call syntax" --> cf. "apply" dans aide Python - args = (unistr(msg),) + args - logger.info(*args, **kwargs) - -def warning(msg, *args, **kwargs): - """Pour journaliser un évènement de niveau WARNING, qui sera affiché sur la - console et enregistré dans différents journaux. - Exemple: un fichier refusé. - - @param msg: le message de l'évènement à journaliser - @type msg: str, unicode, objet quelconque - - @param exc_info: quand ce paramètre vaut True ou 1, l'exception en cours est - journalisée avec le message. - @type exc_info: bool, int, str, ... - """ - # syntaxe bizarre: "extended call syntax" --> cf. "apply" dans aide Python - args = (unistr(msg),) + args - logger.warning(*args, **kwargs) - -def important(msg, *args, **kwargs): - """Pour journaliser un évènement de niveau IMPORTANT (entre WARNING ET ERROR). - Exemple: le début et la fin d'un transfert. - - @param msg: le message de l'évènement à journaliser - @type msg: str, unicode, objet quelconque - - @param exc_info: quand ce paramètre vaut True ou 1, l'exception en cours est - journalisée avec le message. - @type exc_info: bool, int, str, ... - """ - # syntaxe bizarre: "extended call syntax" --> cf. "apply" dans aide Python - args = (IMPORTANT, unistr(msg)) + args - logger.log(*args, **kwargs) - -def error(msg, *args, **kwargs): - """Pour journaliser un évènement de niveau ERROR, qui sera affiché sur la - console et enregistré dans différents journaux. - Exemple: une erreur anormale, qui ne nécessite pas l'arrêt du processus. - - @param msg: le message de l'évènement à journaliser - @type msg: str, unicode, objet quelconque - - @param exc_info: quand ce paramètre vaut True ou 1, l'exception en cours est - journalisée avec le message. - @type exc_info: bool, int, str, ... - """ - # syntaxe bizarre: "extended call syntax" --> cf. "apply" dans aide Python - args = (unistr(msg),) + args - logger.error(*args, **kwargs) - -def exception(msg, *args, **kwargs): - """Pour journaliser un évènement de niveau ERROR avec le texte complet de - l'exception en cours, qui sera affiché sur la console et enregistré dans - différents journaux. Cette fonction doit normalement être appelée dans un - bloc "except". - Exemple: une erreur anormale, qui ne nécessite pas l'arrêt du processus. - - @param msg: le message de l'évènement à journaliser - @type msg: str, unicode, objet quelconque - """ - args = (unistr(msg),) + args - logger.exception(*args, **kwargs) - -def critical(msg, *args, **kwargs): - """Pour journaliser un évènement de niveau CRITICAL, qui sera affiché sur la - console et enregistré dans différents journaux. - Exemple: une erreur critique, qui nécessite l'arrêt du processus. - - @param msg: le message de l'évènement à journaliser - @type msg: str, unicode, objet quelconque - - @param exc_info: quand ce paramètre vaut True ou 1, l'exception en cours est - journalisée avec le message. - @type exc_info: bool, int, str, ... - """ - args = (unistr(msg),) + args - logger.critical(*args, **kwargs) - - -#=== PROGRAMME PRINCIPAL (test) =============================================== - -if __name__ == "__main__": - print "-----------------------------" - print "TEST DU MODULE Journal.py:" - print "-----------------------------" - print "" - #mode_debug(True) -## init_journal() -## for niv in [ -## logging.DEBUG, -## INFO2, -## logging.INFO, -## logging.WARNING, -## IMPORTANT, -## logging.ERROR, -## logging.CRITICAL, -## ]: -## nom = logging.getLevelName(niv) -## niv2 = logging.getLevelName(nom) -## print "niveau %s = %d" % (nom,niv2) -## debug("évènement de niveau DEBUG") -## info2("évènement de niveau INFO2") -## info("évènement de niveau INFO") -## warning("évènement de niveau WARNING") -## important("évènement de niveau IMPORTANT") -## error("évènement de niveau ERROR") -## critical("évènement de niveau CRITICAL") -## print "Je vais maintenant declencher une exception..." -## try: -## a=1/0 -## except: -## exception("exception, évènement de niveau ERROR") - - print 'tests using the new ExtendedLogger class:' - # required to import Politique, because of gettext... - import ExeFilter - # setup a default policy: - import Politique - policy = Politique.Politique() - # enable file logging: - policy.parametres['journal_securite'].valeur = True - policy.parametres['journal_debug'].valeur = True - init_journal(policy, journal_secu='test_secu.log', journal_debug='test_debug.log') - #logging.setLoggerClass(ExtendedLogger) - #logging.basicConfig(level=logging.DEBUG) - log = logger #logging.getLogger('test') - log.debug(u"évènement de niveau DEBUG") - log.info2(u"évènement de niveau INFO2") - log.info(u"évènement de niveau INFO") - log.warning(u"évènement de niveau WARNING") - log.important(u"évènement de niveau IMPORTANT") - log.error(u"évènement de niveau ERROR") - log.critical(u"évènement de niveau CRITICAL") - print "Je vais maintenant declencher une exception..." - try: - a=1/0 - except: - log.exception(u"exception, évènement de niveau ERROR") - +#!/usr/bin/python +# -*- coding: iso-8859-1 -*- +""" +Journal - ExeFilter + +Module qui prend en charge la journalisation et l'affichage des événements. + +Les fonctions du module permettent de journaliser les évènements à l'écran sur +la console, dans un fichier texte et/ou un serveur syslog via UDP. + +Ce fichier fait partie du projet ExeFilter. +URL du projet: U{http://www.decalage.info/exefilter} + +@organization: DGA/CELAR +@author: U{Philippe Lagadec} +@author: U{Arnaud Kerréneur} +@author: U{Tanguy Vinceleux} + +@contact: U{Philippe Lagadec} + +@copyright: DGA/CELAR 2004-2008 +@copyright: NATO/NC3A 2008-2010 (modifications PL apres ExeFilter v1.1.0) + +@license: CeCILL (open-source compatible GPL) + cf. code source ou fichier LICENCE.txt joint + see LICENCE.txt + +@version: 1.04 + +@status: beta +""" + +#============================================================================== +__docformat__ = 'epytext en' + +#__author__ = "Philippe Lagadec, Tanguy Vinceleux, Arnaud Kerréneur (DGA/CELAR)" +__date__ = "2010-12-03" +__version__ = "1.05" + +#------------------------------------------------------------------------------ +# LICENCE pour le projet ExeFilter: + +# Copyright DGA/CELAR 2004-2008 +# Copyright NATO/NC3A 2008-2010 (modifications PL apres ExeFilter v1.1.0) +# Auteurs: +# - Philippe Lagadec (PL) - philippe.lagadec(a)laposte.net +# - Arnaud Kerréneur (AK) - arnaud.kerreneur(a)dga.defense.gouv.fr +# - Tanguy Vinceleux (TV) - tanguy.vinceleux(a)dga.defense.gouv.fr +# +# Ce logiciel est régi par la licence CeCILL soumise au droit français et +# respectant les principes de diffusion des logiciels libres. Vous pouvez +# utiliser, modifier et/ou redistribuer ce programme sous les conditions +# de la licence CeCILL telle que diffusée par le CEA, le CNRS et l'INRIA +# sur le site "http://www.cecill.info". Une copie de cette licence est jointe +# dans les fichiers Licence_CeCILL_V2-fr.html et Licence_CeCILL_V2-en.html. +# +# En contrepartie de l'accessibilité au code source et des droits de copie, +# de modification et de redistribution accordés par cette licence, il n'est +# offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, +# seule une responsabilité restreinte pèse sur l'auteur du programme, le +# titulaire des droits patrimoniaux et les concédants successifs. +# +# A cet égard l'attention de l'utilisateur est attirée sur les risques +# associés au chargement, à l'utilisation, à la modification et/ou au +# développement et à la reproduction du logiciel par l'utilisateur étant +# donné sa spécificité de logiciel libre, qui peut le rendre complexe à +# manipuler et qui le réserve donc à des développeurs et des professionnels +# avertis possédant des connaissances informatiques approfondies. Les +# utilisateurs sont donc invités à charger et tester l'adéquation du +# logiciel à leurs besoins dans des conditions permettant d'assurer la +# sécurité de leurs systèmes et ou de leurs données et, plus généralement, +# à l'utiliser et l'exploiter dans les mêmes conditions de sécurité. +# +# Le fait que vous puissiez accéder à cet en-tête signifie que vous avez +# pris connaissance de la licence CeCILL, et que vous en avez accepté les +# termes. + +#------------------------------------------------------------------------------ +# HISTORIQUE: +# 19/01/2005 v0.01 AK: - 1ère version +# 2005-2007 PL,AK: - nombreuses évolutions + contributions de Y. Bidan +# et C. Catherin +# 12/01/2007 v1.00 PL: - version 1.00 officielle +# 2007-09-20 v1.01 PL: - licence CeCILL +# 2010-02-07 v1.02 PL: - removed path module import +# 2010-02-23 v1.03 PL: - updated plx import +# 2010-11-26 v1.04 PL: - code cleanup, use ExtendedLogger class +# 2010-12-03 v1.05 PL: - append to log files instead of overwriting +# - separate function init_console_logging + +#------------------------------------------------------------------------------ +# TODO: +# + split init_journal into several functions: +# - init_console to setup console logging at the root level only once +# - set_logfile(s) to setup logfile(s) for each session, at the logger level +# - set_syslog to setup syslog the same way +# + when used as a library, xf should only have a NullHandler +# + fix encoding issue on Windows console +# + option to use a rotating file handler to limit log file size +# ? each new call to init_journal should use a different name for the logger +# (to support multithreading) +# + do not use a global logger variable, but a per-session logger object +# + init_jounal: return an ExtendedLogger object +# + remove obsolete module-level functions, once other modules are fixed +# - corriger bug syslog: messages dupliqués au 2ème transfert d'affilée depuis +# l'IHM (puis retour à la normale au 3ème transfert) +# - corriger les docstrings pour éviter les warnings epydoc +# - issue when ExeFilter is embedded into another application that uses logging: +# it should be possible to create a logger object without handlers. +# (handlers should only be defined by the main app to avoid duplication) +# ? rename "INFO2" level to "DETAIL" or "TECH" to be clearer? + +# EVOLUTIONS ENVISAGEES: +# - avec Python 2.4.2, Formatter_Latin1 n'est plus nécessaire, utiliser le +# nouveau paramètre encoding de FileHandler +#------------------------------------------------------------------------------ + + +#=== IMPORTS ================================================================== + +import logging, logging.handlers, os.path, sys + +# modules du projet: +from commun import * +import commun +from thirdparty.plx.plx import * + + +#=== CONSTANTES =============================================================== + +# OBSOLETE: +SYSLOG = 0 +FICHIER = 1 +SYS_FIC = 2 + +# niveaux de jouralisation ajoutés aux niveaux du module standard logging: +# (cf. notes de développements pour plus de détails) +##CRITICAL = 50 # Erreur fatale, le processus ne peut continuer +##ERROR = 40 # Erreur grave mais non fatale +IMPORTANT = 35 # Information importante à toujours journaliser, mais qui + # n'est pas un warning. +##WARNING = 30 # Avertissement, problème potentiel +##INFO = 20 # Information normale +INFO2 = 15 # Information technique +##DEBUG = 10 # Information de déboguage pour développeur +##NOTSET = 0 + +#=== VARIABLES GLOBALES ======================================================= + +# flag pour éviter de faire 2 init_journal() +init_effectue = False + +# global logger object +logger = None + +#=== CLASSES ================================================================== + +#------------------------------------------------------------------------------ +# classe Formatter_console +#-------------------------- + +class Formatter_console (logging.Formatter): + """classe qui hérite de logging.Formatter et ajoute une conversion + grâce au codec adapté à l'OS pour un affichage correct sur la + console. (CMD sous Windows) + """ + + def format(self, record): + # on appelle d'abord le format() d'origine pour obtenir la chaîne: + chaine = logging.Formatter.format(self, record) + # ensuite on convertit le résultat si besoin: + return str_console(chaine, errors='replace') + +#------------------------------------------------------------------------------ +# classe Formatter_Latin1 +#------------------------- + +class Formatter_Latin1 (logging.Formatter): + """classe qui hérite de logging.Formatter et ajoute une conversion + grâce au codec latin_1 pour les messages Unicode. + (Sinon par défaut Python emploie UTF-8) + """ + + def format(self, record): + # on appelle d'abord le format() d'origine pour obtenir la chaîne: + chaine = logging.Formatter.format(self, record) + # ensuite on convertit le résultat en str si c'est de l'Unicode: + return str_lat1(chaine, errors='replace') + + +class ExtendedLogger (logging.Logger): + """ + extended Logger class for ExeFilter, based on logging.Logger: + - with additional methods to support more logging levels + - with conversion from strings to Unicode if needed + """ + + def debug(self, msg, *args, **kwargs): + """Pour journaliser un évènement de niveau DEBUG, pour le journal de + débogage technique en mode DEBUG pour le développement uniquement. + + @param msg: le message de l'évènement à journaliser + @type msg: str, unicode, objet quelconque + + @param exc_info: quand ce paramètre vaut True ou 1, l'exception en cours est + journalisée avec le message. + @type exc_info: bool, int, str, ... + """ + # ci-dessous la virgule est indispensable sinon (unistr(chaine)) est une + # chaîne et non un tuple... + args = (unistr(msg),) + args + # syntaxe bizarre: "extended call syntax" --> cf. "apply" dans aide Python + logging.Logger.debug(self, *args, **kwargs) + + + def info2(self, msg, *args, **kwargs): + """Pour journaliser un évènement de niveau INFO2, pour le journal de + débogage technique. + + @param msg: le message de l'évènement à journaliser + @type msg: str, unicode, objet quelconque + + @param exc_info: quand ce paramètre vaut True ou 1, l'exception en cours est + journalisée avec le message. + @type exc_info: bool, int, str, ... + """ + # syntaxe bizarre: "extended call syntax" --> cf. "apply" dans aide Python + args = (INFO2, unistr(msg)) + args + logging.Logger.log(self, *args, **kwargs) + + + def info(self, msg, *args, **kwargs): + """Pour journaliser un évènement de niveau INFO, qui sera affiché sur la + console et enregistré dans différents journaux. + Exemple: un fichier nettoyé ou accepté. + + @param msg: le message de l'évènement à journaliser + @type msg: str, unicode, objet quelconque + + @param exc_info: quand ce paramètre vaut True ou 1, l'exception en cours est + journalisée avec le message. + @type exc_info: bool, int, str, ... + """ + # syntaxe bizarre: "extended call syntax" --> cf. "apply" dans aide Python + args = (unistr(msg),) + args + logging.Logger.info(self, *args, **kwargs) + + + def warning(self, msg, *args, **kwargs): + """Pour journaliser un évènement de niveau WARNING, qui sera affiché sur la + console et enregistré dans différents journaux. + Exemple: un fichier refusé. + + @param msg: le message de l'évènement à journaliser + @type msg: str, unicode, objet quelconque + + @param exc_info: quand ce paramètre vaut True ou 1, l'exception en cours est + journalisée avec le message. + @type exc_info: bool, int, str, ... + """ + # syntaxe bizarre: "extended call syntax" --> cf. "apply" dans aide Python + args = (unistr(msg),) + args + logging.Logger.warning(self, *args, **kwargs) + + + def important(self, msg, *args, **kwargs): + """Pour journaliser un évènement de niveau IMPORTANT (entre WARNING ET ERROR). + Exemple: le début et la fin d'un transfert. + + @param msg: le message de l'évènement à journaliser + @type msg: str, unicode, objet quelconque + + @param exc_info: quand ce paramètre vaut True ou 1, l'exception en cours est + journalisée avec le message. + @type exc_info: bool, int, str, ... + """ + # syntaxe bizarre: "extended call syntax" --> cf. "apply" dans aide Python + args = (IMPORTANT, unistr(msg)) + args + logging.Logger.log(self, *args, **kwargs) + + + def error(self, msg, *args, **kwargs): + """Pour journaliser un évènement de niveau ERROR, qui sera affiché sur la + console et enregistré dans différents journaux. + Exemple: une erreur anormale, qui ne nécessite pas l'arrêt du processus. + + @param msg: le message de l'évènement à journaliser + @type msg: str, unicode, objet quelconque + + @param exc_info: quand ce paramètre vaut True ou 1, l'exception en cours est + journalisée avec le message. + @type exc_info: bool, int, str, ... + """ + # syntaxe bizarre: "extended call syntax" --> cf. "apply" dans aide Python + args = (unistr(msg),) + args + logging.Logger.error(self, *args, **kwargs) + + + def exception(self, msg, *args, **kwargs): + """Pour journaliser un évènement de niveau ERROR avec le texte complet de + l'exception en cours, qui sera affiché sur la console et enregistré dans + différents journaux. Cette fonction doit normalement être appelée dans un + bloc "except". + Exemple: une erreur anormale, qui ne nécessite pas l'arrêt du processus. + + @param msg: le message de l'évènement à journaliser + @type msg: str, unicode, objet quelconque + """ + args = (unistr(msg),) + args + logging.Logger.exception(self, *args, **kwargs) + + + def critical(self, msg, *args, **kwargs): + """Pour journaliser un évènement de niveau CRITICAL, qui sera affiché sur la + console et enregistré dans différents journaux. + Exemple: une erreur critique, qui nécessite l'arrêt du processus. + + @param msg: le message de l'évènement à journaliser + @type msg: str, unicode, objet quelconque + + @param exc_info: quand ce paramètre vaut True ou 1, l'exception en cours est + journalisée avec le message. + @type exc_info: bool, int, str, ... + """ + args = (unistr(msg),) + args + logging.Logger.critical(self, *args, **kwargs) + + +#=== INITIALISATION =========================================================== + +# set ExtendedLogger class as default instead of logging.Logger: +logging.setLoggerClass(ExtendedLogger) +# set global logger +logger = logging.getLogger('ExeFilter') + +# two additional logging levels: +logging.addLevelName (INFO2, "INFO2") +logging.addLevelName (IMPORTANT, "IMPORTANT") + + + +#=== FONCTIONS ================================================================ + +def init_console_logging(): + """ + initialize a console handler for the root logger, so that messages are + displayed on the console. + Should only be called if the main application does not already setup logging + handlers. + Should be called early, before any call to logging. + """ + # get the root logger + rootLogger = logging.getLogger('') + # make sure the root logger accepts up to debug messages: + rootLogger.setLevel(logging.DEBUG) + + # 1) on ajoute un affichage console de niveau INFO, + # qui n'affiche que les messages sans date/niveau: + log_console = logging.StreamHandler(sys.stdout) + log_console.setLevel(logging.INFO) + # si on est en mode debug, niveau=DEBUG: + if commun.mode_debug(): + log_console.setLevel(logging.DEBUG) + log_console.setFormatter(Formatter_console('%(message)s')) + rootLogger.addHandler(log_console) + + +#------------------------------------------------------------------------------ +# init_journal +#------------------- + +def init_journal (politique, journal_secu=None, journal_debug=None): + """ + Initialise the logging system for a session. Handlers to use log files and + syslog are enabled for the logger object according to the policy. + + - politique: policy object + - journal_secu: filename/path of the security log file (INFO level) + - journal_debug: filename/path of the debug log file (INFO2 level) + + Note: the policy controls if log files are enabled or not + """ +## # on récupère le Logger racine du module logging: +## rootLogger = logging.getLogger('') + # make sure the logger object accepts up to debug messages: + logger.setLevel(logging.DEBUG) + + # on supprime tout handler qui serait déjà configuré, pour éviter les + # problèmes éventuels (ça arrive): + for h in logger.handlers: + try: + h.flush() + except: + pass + h.close() + logger.removeHandler(h) + + # 2) on ajoute une sortie vers un fichier de niveau INFO2, + # qui affiche les messages avec date/niveau: + if politique.parametres['journal_debug'].valeur: + # append to existing log file instead of replacing it ('w' mode): + log_fichier = logging.FileHandler(journal_debug) #, 'w') + log_fichier.setLevel(INFO2) + # si on est en mode debug, niveau=DEBUG: + if commun.mode_debug(): + log_fichier.setLevel(logging.DEBUG) + log_fichier.setFormatter(Formatter_Latin1( + #fmt = '%(asctime)s %(name)-12s %(levelname)-9s %(message)s', + fmt = '%(asctime)s %(levelname)-9s %(message)s', + datefmt = '%d/%m/%y %H:%M')) + logger.addHandler(log_fichier) + + # 3) on ajoute une sortie vers un fichier (journal de sécurité) de niveau INFO, + # qui affiche les messages avec date/niveau: + if politique.parametres['journal_securite'].valeur: + # append to existing log file instead of replacing it ('w' mode): + log_fichier2 = logging.FileHandler(journal_secu) #, 'w') + log_fichier2.setLevel(logging.INFO) + log_fichier2.setFormatter(Formatter_Latin1( + #fmt = '%(asctime)s %(name)-12s %(levelname)-9s %(message)s', + fmt = '%(asctime)s %(levelname)-9s %(message)s', + datefmt = '%d/%m/%y %H:%M')) + logger.addHandler(log_fichier2) + + # 4) on ajoute une sortie vers un serveur syslog de niveau WARNING, + # qui affiche les messages avec date/niveau: + # On doit d'abord corriger le tableau de conversion des priorités logging/syslog + # (sinon levée d'exceptions pour les niveaux que nous avons ajoutés...) + # notre niveau INFO correspond au niveau NOTICE de syslog: + if politique.parametres['journal_syslog'].valeur: + logging.handlers.SysLogHandler.priority_names['info'] = logging.handlers.SysLogHandler.LOG_NOTICE + # notre niveau INFO2 correspond au niveau INFO de syslog: + logging.handlers.SysLogHandler.priority_names['info2'] = logging.handlers.SysLogHandler.LOG_INFO + # notre niveau IMPORTANT correspond au niveau WARNING de syslog: + logging.handlers.SysLogHandler.priority_names['important'] = logging.handlers.SysLogHandler.LOG_WARNING + # ensuite on peut créer notre instance de SysLogHandler: + adresse_ip = politique.parametres['serveur_syslog'].valeur + port_udp = politique.parametres['port_syslog'].valeur + log_syslog = logging.handlers.SysLogHandler((adresse_ip, port_udp)) + log_syslog.setLevel(logging.WARNING) + #log_syslog.setFormatter(Formatter_Latin1( + # fmt = '%(asctime)s %(name)-12s %(levelname)-9s %(message)s', + # datefmt = '%d/%m/%y %H:%M')) + # syslog n'a pas besoin des infos date/heure et niveau, juste le message: + log_syslog.setFormatter(Formatter_Latin1('%(message)s')) + logger.addHandler(log_syslog) + + # return the logger object (in the future there will be one per session): + return logger + + +#------------------------------------------------------------------------------ +# fermer_journal +#------------------- + +def fermer_journal() : + """ + Ferme proprement l'objet journal + """ + try : + # on récupère le Logger racine du module logging: +## rootLogger = logging.getLogger('') + # on ferme proprement chaque handler: + for h in logger.handlers: + h.flush() + h.close() + logger.removeHandler(h) +## log_console.flush() +## log_console.close() +## log_fichier.flush() +## log_fichier.close() +## log_fichier2.flush() +## log_fichier2.close() +## log_syslog.flush() +## log_syslog.close() +## # puis on les supprime: +## logger.removeHandler(log_console) +## logger.removeHandler(log_fichier) +## logger.removeHandler(log_fichier2) +## logger.removeHandler(log_syslog) + except: + raise + + +#------------------------------------------------------------------------------ +# JOURNALISATION +#------------------- +# cf. aide du module logging + +def debug(msg, *args, **kwargs): + """Pour journaliser un évènement de niveau DEBUG, pour le journal de + débogage technique en mode DEBUG pour le développement uniquement. + + @param msg: le message de l'évènement à journaliser + @type msg: str, unicode, objet quelconque + + @param exc_info: quand ce paramètre vaut True ou 1, l'exception en cours est + journalisée avec le message. + @type exc_info: bool, int, str, ... + """ + # ci-dessous la virgule est indispensable sinon (unistr(chaine)) est une + # chaîne et non un tuple... + args = (unistr(msg),) + args + # syntaxe bizarre: "extended call syntax" --> cf. "apply" dans aide Python + logger.debug(*args, **kwargs) + +def info2(msg, *args, **kwargs): + """Pour journaliser un évènement de niveau INFO2, pour le journal de + débogage technique. + + @param msg: le message de l'évènement à journaliser + @type msg: str, unicode, objet quelconque + + @param exc_info: quand ce paramètre vaut True ou 1, l'exception en cours est + journalisée avec le message. + @type exc_info: bool, int, str, ... + """ + # syntaxe bizarre: "extended call syntax" --> cf. "apply" dans aide Python + args = (INFO2, unistr(msg)) + args + logger.log(*args, **kwargs) + +def info(msg, *args, **kwargs): + """Pour journaliser un évènement de niveau INFO, qui sera affiché sur la + console et enregistré dans différents journaux. + Exemple: un fichier nettoyé ou accepté. + + @param msg: le message de l'évènement à journaliser + @type msg: str, unicode, objet quelconque + + @param exc_info: quand ce paramètre vaut True ou 1, l'exception en cours est + journalisée avec le message. + @type exc_info: bool, int, str, ... + """ + # syntaxe bizarre: "extended call syntax" --> cf. "apply" dans aide Python + args = (unistr(msg),) + args + logger.info(*args, **kwargs) + +def warning(msg, *args, **kwargs): + """Pour journaliser un évènement de niveau WARNING, qui sera affiché sur la + console et enregistré dans différents journaux. + Exemple: un fichier refusé. + + @param msg: le message de l'évènement à journaliser + @type msg: str, unicode, objet quelconque + + @param exc_info: quand ce paramètre vaut True ou 1, l'exception en cours est + journalisée avec le message. + @type exc_info: bool, int, str, ... + """ + # syntaxe bizarre: "extended call syntax" --> cf. "apply" dans aide Python + args = (unistr(msg),) + args + logger.warning(*args, **kwargs) + +def important(msg, *args, **kwargs): + """Pour journaliser un évènement de niveau IMPORTANT (entre WARNING ET ERROR). + Exemple: le début et la fin d'un transfert. + + @param msg: le message de l'évènement à journaliser + @type msg: str, unicode, objet quelconque + + @param exc_info: quand ce paramètre vaut True ou 1, l'exception en cours est + journalisée avec le message. + @type exc_info: bool, int, str, ... + """ + # syntaxe bizarre: "extended call syntax" --> cf. "apply" dans aide Python + args = (IMPORTANT, unistr(msg)) + args + logger.log(*args, **kwargs) + +def error(msg, *args, **kwargs): + """Pour journaliser un évènement de niveau ERROR, qui sera affiché sur la + console et enregistré dans différents journaux. + Exemple: une erreur anormale, qui ne nécessite pas l'arrêt du processus. + + @param msg: le message de l'évènement à journaliser + @type msg: str, unicode, objet quelconque + + @param exc_info: quand ce paramètre vaut True ou 1, l'exception en cours est + journalisée avec le message. + @type exc_info: bool, int, str, ... + """ + # syntaxe bizarre: "extended call syntax" --> cf. "apply" dans aide Python + args = (unistr(msg),) + args + logger.error(*args, **kwargs) + +def exception(msg, *args, **kwargs): + """Pour journaliser un évènement de niveau ERROR avec le texte complet de + l'exception en cours, qui sera affiché sur la console et enregistré dans + différents journaux. Cette fonction doit normalement être appelée dans un + bloc "except". + Exemple: une erreur anormale, qui ne nécessite pas l'arrêt du processus. + + @param msg: le message de l'évènement à journaliser + @type msg: str, unicode, objet quelconque + """ + args = (unistr(msg),) + args + logger.exception(*args, **kwargs) + +def critical(msg, *args, **kwargs): + """Pour journaliser un évènement de niveau CRITICAL, qui sera affiché sur la + console et enregistré dans différents journaux. + Exemple: une erreur critique, qui nécessite l'arrêt du processus. + + @param msg: le message de l'évènement à journaliser + @type msg: str, unicode, objet quelconque + + @param exc_info: quand ce paramètre vaut True ou 1, l'exception en cours est + journalisée avec le message. + @type exc_info: bool, int, str, ... + """ + args = (unistr(msg),) + args + logger.critical(*args, **kwargs) + + +#=== PROGRAMME PRINCIPAL (test) =============================================== + +if __name__ == "__main__": + print("-----------------------------") + print("TEST DU MODULE Journal.py:") + print("-----------------------------") + print("") + #mode_debug(True) +## init_journal() +## for niv in [ +## logging.DEBUG, +## INFO2, +## logging.INFO, +## logging.WARNING, +## IMPORTANT, +## logging.ERROR, +## logging.CRITICAL, +## ]: +## nom = logging.getLevelName(niv) +## niv2 = logging.getLevelName(nom) +## print "niveau %s = %d" % (nom,niv2) +## debug("évènement de niveau DEBUG") +## info2("évènement de niveau INFO2") +## info("évènement de niveau INFO") +## warning("évènement de niveau WARNING") +## important("évènement de niveau IMPORTANT") +## error("évènement de niveau ERROR") +## critical("évènement de niveau CRITICAL") +## print "Je vais maintenant declencher une exception..." +## try: +## a=1/0 +## except: +## exception("exception, évènement de niveau ERROR") + + print('tests using the new ExtendedLogger class:') + # required to import Politique, because of gettext... + import ExeFilter + # setup a default policy: + import Politique + policy = Politique.Politique() + # enable file logging: + policy.parametres['journal_securite'].valeur = True + policy.parametres['journal_debug'].valeur = True + init_journal(policy, journal_secu='test_secu.log', journal_debug='test_debug.log') + #logging.setLoggerClass(ExtendedLogger) + #logging.basicConfig(level=logging.DEBUG) + log = logger #logging.getLogger('test') + log.debug(u"évènement de niveau DEBUG") + log.info2(u"évènement de niveau INFO2") + log.info(u"évènement de niveau INFO") + log.warning(u"évènement de niveau WARNING") + log.important(u"évènement de niveau IMPORTANT") + log.error(u"évènement de niveau ERROR") + log.critical(u"évènement de niveau CRITICAL") + print("Je vais maintenant declencher une exception...") + try: + a=1/0 + except: + log.exception(u"exception, évènement de niveau ERROR") + diff --git a/Parametres.py b/Parametres.py index fb8e4c2..0061f90 100644 --- a/Parametres.py +++ b/Parametres.py @@ -1,487 +1,490 @@ -#!/usr/bin/python -# -*- coding: iso-8859-1 -*- -""" -Parametres - ExeFilter - -Le module Parametres permet de gérer les paramètres des différents modules -et filtres, grâce à la classe L{Parametres.Parametre}. - -Les paramètres d'un filtre doivent être regroupés dans un dictionnaire indexé -suivant les codes des paramètres, et manipulés grâce aux fonctions de ce module. - -Ce fichier fait partie du projet ExeFilter. -URL du projet: http://www.decalage.info/exefilter - -@organization: DGA/CELAR -@author: U{Philippe Lagadec} - -@contact: U{Philippe Lagadec} - -@copyright: DGA/CELAR 2004-2008 -@copyright: NATO/NC3A 2008 (modifications PL apres v1.1.0) - -@license: CeCILL (open-source compatible GPL) - cf. code source ou fichier LICENCE.txt joint - -@version: 1.02 - -@status: beta -""" -#============================================================================== -__docformat__ = 'epytext en' - -__date__ = "2008-03-24" -__version__ = "1.02" - -#------------------------------------------------------------------------------ -# LICENCE pour le projet ExeFilter: - -# Copyright DGA/CELAR 2004-2008 -# Copyright NATO/NC3A 2008 (PL changes after v1.1.0) -# Auteurs: -# - Philippe Lagadec (PL) - philippe.lagadec(a)laposte.net -# - Arnaud Kerréneur (AK) - arnaud.kerreneur(a)dga.defense.gouv.fr -# - Tanguy Vinceleux (TV) - tanguy.vinceleux(a)dga.defense.gouv.fr -# -# Ce logiciel est régi par la licence CeCILL soumise au droit français et -# respectant les principes de diffusion des logiciels libres. Vous pouvez -# utiliser, modifier et/ou redistribuer ce programme sous les conditions -# de la licence CeCILL telle que diffusée par le CEA, le CNRS et l'INRIA -# sur le site "http://www.cecill.info". Une copie de cette licence est jointe -# dans les fichiers Licence_CeCILL_V2-fr.html et Licence_CeCILL_V2-en.html. -# -# En contrepartie de l'accessibilité au code source et des droits de copie, -# de modification et de redistribution accordés par cette licence, il n'est -# offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, -# seule une responsabilité restreinte pèse sur l'auteur du programme, le -# titulaire des droits patrimoniaux et les concédants successifs. -# -# A cet égard l'attention de l'utilisateur est attirée sur les risques -# associés au chargement, à l'utilisation, à la modification et/ou au -# développement et à la reproduction du logiciel par l'utilisateur étant -# donné sa spécificité de logiciel libre, qui peut le rendre complexe à -# manipuler et qui le réserve donc à des développeurs et des professionnels -# avertis possédant des connaissances informatiques approfondies. Les -# utilisateurs sont donc invités à charger et tester l'adéquation du -# logiciel à leurs besoins dans des conditions permettant d'assurer la -# sécurité de leurs systèmes et ou de leurs données et, plus généralement, -# à l'utiliser et l'exploiter dans les mêmes conditions de sécurité. -# -# Le fait que vous puissiez accéder à cet en-tête signifie que vous avez -# pris connaissance de la licence CeCILL, et que vous en avez accepté les -# termes. - -#------------------------------------------------------------------------------ -# HISTORIQUE: -# 30/09/2005 v0.01 PL: - 1ère version -# 12/10/2005 v0.02 PL: - ajout et correction de quelques commentaires -# - modif complète des fonctions, pour permettre -# l'utilisation d'objets ConfigParser ou de fichiers -# - fonction ajouter_parametre transformée en méthode -# 12/01/2007 v1.00 PL: - version 1.00 officielle -# 2007-09-18 v1.01 PL: - ajout licence CeCILL -# 2008-03-27 v1.02 PL: - bug corrige dans Parametre.set - -#------------------------------------------------------------------------------ -# A FAIRE: -# + ajouter gestion de type=list, pour les listes d'extensions, avec conversion -# de chaînes en splittant suivant les virgules + strip espaces -# + ajouter attribut valeurs_possibles=None/liste, avec vérif dans set() -# + vérifier si p1=p2 fait bien une recopie de l'objet et non pas juste de la -# référence, sinon il faudra utiliser le module copy -# ? ajouter attribut valeurs_restrictives = "croissantes"/"decroissantes" ou -# True/False + méthodes pour pouvoir comparer + moyen pour fusionner en ne -# gardant que les params les + restrictifs ? -# - fonction reinit pour appliquer reinit sur chaque param d'un dico -# - mettre à jour docstring en utilisant mieux les fonctions d'epydoc, et -# déclarer les constantes. -#------------------------------------------------------------------------------ - -#=== IMPORTS ================================================================== -import sys -import ConfigParser -import os, copy - -#=== VARIABLES GLOBALES ======================================================= - - -#=== CONSTANTES =============================================================== - -__doc__ += """ -@var VALEURS_VRAIES: valeurs pouvant être utilisées pour un paramètre bool True -@var VALEURS_FAUSSES: valeurs pouvant être utilisées pour un paramètre bool False -""" -# valeurs pour convertir une chaîne en booléen (toujours en minuscules) -VALEURS_VRAIES = ["1", "oui", "o", "yes", "y", "vrai", "vraie", "v", "t", "true", "on"] -VALEURS_FAUSSES = ["0", "non", "n", "no", "faux", "fausse", "f", "false", "off"] - -#=== CLASSES ================================================================== - -#------------------------------------------------------------------------------ -# classe Parametre -#------------------- - -class Parametre: - """ - classe permettant de gérer un paramètre avec une valeur. - - @ivar code: nom court utilisé pour indexer un dictionnaire de paramètres, - et pour utiliser ce paramètre dans un fichier de configuration. - Doit être en minuscules, sans espaces et sans accents. - Exemple: "supprimer_macros" - @type code: str, unicode - - @ivar nom: Nom simple décrivant le paramètre, utilisable par exemple - dans une IHM, et comme commentaire dans un fichier de config. - Exemple: "Supprimer les macros" - @type nom: str, unicode - - @ivar description: (optionnel) Description plus longue et détaillée - du paramètre, utilisable comme aide contextuelle dans l'IHM et - comme commentaire dans un fichier de config. - Exemple: "Les macros peuvent contenir du code malveillant." - @type description: str, unicode - - @ivar type: classe Python de la valeur, par exemple bool, str, int, list, ... - @type type: classe Python - - @ivar valeur: valeur du paramètre, doit correspondre au type. - - @ivar valeur_defaut: valeur par défaut du paramètre - """ - - def __init__ (self, code, type, nom="", description="", valeur=None, - valeur_defaut=None): - """Constructeur d'objet Parametre. - (cf. classe Parametre pour la description des attributs) - - code et type sont obligatoires. - """ - self.code = code - self.nom = nom - self.description = description - self.type = type - # conversion de la valeur suivant le type, au cas où: - self.valeur_defaut = type(valeur_defaut) - # on doit initialiser la valeur à None, pour set(): - self.valeur = None - self.set(valeur) - -## def __set__(self, instance, value): -## """Pour fixer la valeur d'un paramètre, avec la syntaxe simple -## Parametre = valeur -## Cette méthode convertit automatiquement la valeur suivant le -## type déclaré du paramètre. -## """ -## self.valeur = self.type(value) - - def set(self, valeur): - """Pour fixer la valeur d'un paramètre. - Cette méthode convertit automatiquement la valeur suivant le - type déclaré du paramètre. - - - Si le type est bool et que la valeur est une chaîne les valeurs - spéciales suivantes sont prises en compte: - - True = 1, oui, O, yes, Y, T, True, on, ... - - False = 0, non, N, no, F, False, off, ... - - (cf. constantes L{Parametres.VALEURS_VRAIES} et - L{Parametres.VALEURS_FAUSSES}) - - - Si c'est un autre type (int, str, ...) la conversion automatique du - type est employée. - - - Si la valeur est None, la valeur par défaut est employée - si la valeur d'origine est aussi None (1ère initialisation) - (sinon pas de modification) - """ - # si aucune valeur fournie et qu'il n'y en avait pas déjà, - # on prend la valeur par défaut: - if valeur == None: - if self.valeur == None: - self.valeur = self.valeur_defaut - else: - # si la valeur est une chaîne (str ou unicode) et que le - # paramètre est un booléen, conversion: - if self.type == bool: - if isinstance(valeur, str) or isinstance(valeur, unicode): - if valeur.strip().lower() in VALEURS_VRAIES: - self.valeur = True - elif valeur.strip().lower() in VALEURS_FAUSSES: - self.valeur = False - else: - self.valeur = bool(valeur) - else: - # sinon on utilise la conversion automatique du type - # d'objet correspondant au paramètre: - self.valeur = self.type(valeur) - - - def ajouter (self, dico_parametres): - """Pour ajouter l'objet Parametre au dictionnaire dico_parametres. - - Exemple:: - Parametre("supprimer_macros", bool, nom="Supprimer les macros", - valeur_defaut=True).ajouter(Filtre_Word.parametres) - """ - # ATTENTION: ici on ne fait que recopier une référence à l'objet - # Parametre dans le dictionnaire. Si on l'ajoute à plusieurs dicos, - # l'objet sera partagé entre tous les dicos. - dico_parametres[self.code] = self - - - def reinit(self): - """Pour réinitialiser le paramètre à sa valeur par défaut. - """ - self.valeur = self.valeur_defaut - - - def __str__(self): - """Convertit la valeur du paramètre en chaîne, en 1 ou 0 si c'est - un bool. - - exemple d'utilisation: print str(parametre["activer_filtre"]) - """ - if self.type == bool: - if self.valeur: return "1" - else: return "0" - else: - return str(self.valeur) - - def debug(self): - """Retourne une chaîne décrivant complètement le paramètre - et ses attributs, utile surtout pour le débogage.""" - return "%s = %s\n" % (self.code, repr(self.valeur)) + \ - "- nom: %s\n" % self.nom + \ - "- description: %s\n" % self.description + \ - "- type: %s\n" % str(self.type) + \ - "- valeur par defaut: %s" % repr(self.valeur_defaut) - - -#=== FONCTIONS ================================================================ - -#------------------------------------------------------------------------------ -# importer -#--------------------- - -def importer (dico_parametres, nouveaux_parametres): - """Pour importer les valeurs du dictionnaire nouveaux_parametres - dans le dictionnaire dico_parametres. - nouveaux_parametres doit contenir des valeurs indexées par - les codes de paramètres, ou bien des objets Parametre. - """ - for code in nouveaux_parametres: - valeur = nouveaux_parametres[code] - if isinstance (valeur, Parametre): - # on utilise le module copy pour dupliquer le paramètre, car sinon - # on ne recopie qu'une référence au même paramètre: - dico_parametres[code] = copy.deepcopy(valeur) - else: - dico_parametres[code].set(valeur) - #Ancienne version, avec nouveaux_parametres un dico de Parametre - #dico_parametres.update(nouveaux_parametres) - -#------------------------------------------------------------------------------ -# lire_config -#--------------------- - -def lire_config (dico_parametres, config, section): - """Pour mettre à jour les valeurs du dictionnaire dico_parametres, - en lisant soit: - - un fichier de configuration au format INI, dans la section indiquée, - fourni sous forme d'objet file ou le nom du fichier - - un objet (Raw)ConfigParser - - Si un paramètre du fichier n'existe pas dans dico_parametres, il est - simplement ignoré. - - config : nom de fichier à lire, objet file, ou ConfigParser - section : chaîne - """ - if isinstance(config, ConfigParser.RawConfigParser): - # config est un objet (Raw)ConfigParser - cfg = config - else: - # on crée un ConfigParser vide: - cfg = ConfigParser.SafeConfigParser() - if isinstance(config, str) or isinstance(config, unicode): - # config est un nom de fichier - f = file(config, 'r') - cfg.readfp(f) - f.close() - elif isinstance(config, file): - # config est un fichier déjà ouvert - cfg.readfp(config) - else: - # sinon c'est une erreur - raise TypeError - # ensuite on peut exploiter la config lue, si la section demandée est - # présente: - if cfg.has_section(section): - for code, valeur in cfg.items(section): - if code in dico_parametres: - dico_parametres[code].set(valeur) - - -#------------------------------------------------------------------------------ -# ecrire_config -#--------------------- - -def ecrire_config (dico_parametres, config, section): - """Pour écrire les valeurs du dictionnaire dico_parametres dans - un objet ConfigParser, dans la section indiquée. - Si la section existait déjà, elle est simplement mise à jour. - - @param config: objet ConfigParser à écrire - @param section: chaîne - """ - if not config.has_section(section): - config.add_section(section) - for code in dico_parametres: - config.set(section, code, str(dico_parametres[code])) - -def lire_fichier(config, fichier): - """Pour lire un fichier dans un objet ConfigParser. - - @param config: objet ConfigParser à écrire - @param fichier: fichier à lire - @type fichier: nom de fichier ou objet file - """ - if isinstance(fichier, file): - # directement si c'est un objet file - config.readfp(fichier) - else: - # sinon on ouvre le fichier - f = file(fichier) - config.readfp(f) - f.close() - -def ecrire_fichier(config, fichier): - """Pour écrire un objet ConfigParser dans un fichier. - - @param config : objet ConfigParser à lire - @param fichier: fichier à écrire - @type fichier: nom de fichier ou objet file - """ - if isinstance(fichier, file): - # directement si c'est un objet file (par exemple sys.stdout) - config.write(fichier) - else: - # sinon on crée le fichier - f = file(fichier, "w") - config.write(f) - f.close() - - -#=== PROGRAMME PRINCIPAL (test) =============================================== - -if __name__ == "__main__": - print "-----------------------------" - print "TEST DU MODULE Parametres.py:" - print "-----------------------------" - print "" - - # fonction utiles uniquement pour les tests: - - def print_fichier(fichier): - """Pour afficher un fichier à l'écran.""" - f = file(fichier) - print f.read() - f.close() - - # BOOLEEN : - p1 = Parametre("p_bool", bool, nom="valeur booleenne", - description="Ceci est un parametre booleen.", - valeur_defaut=False) - print p1.debug() - p1.set(True) - print p1.debug() - p1.set("0") - print p1.debug() - print "test des valeurs bool:" - for valeur in VALEURS_VRAIES: - p1.set(valeur) - if p1.valeur == True: - print "%s: OK" % valeur - else: - print "%s: NOK !" % valeur - print p1.debug() - for valeur in VALEURS_FAUSSES: - p1.set(valeur) - if p1.valeur == False: - print "%s: OK" % valeur - else: - print "%s: NOK !" % valeur - print p1.debug() - print "-----------------------------" - - # ENTIER : - p = Parametre("p_entier", int, nom="valeur entiere", - description="Ceci est un parametre entier.", - valeur_defaut=0) - print p.debug() - p.set(17) - print p.debug() - p.set("254") - print p.debug() - p.set(True) - print p.debug() - print "-----------------------------" - - # CHAINE : - p = Parametre("p_chaine", str, nom="valeur chaine", - description="Ceci est un parametre chaine.", - valeur_defaut="(vide)") - print p.debug() - p.set("ceci est une chaine") - print p.debug() - p.set(1789) - print p.debug() - p.set(True) - print p.debug() - - print "-----------------------------" - print "test de dictionnaires de parametres:" - params = {} - Parametre("p_bool", bool, valeur_defaut=False).ajouter(params) - Parametre("p_int", int, valeur_defaut=0).ajouter(params) - Parametre("p_str", str, valeur_defaut="").ajouter(params) - print "parametres avant:" - for param in params: - print params[param].debug() - print "" - params2 = {} - Parametre("p_bool", bool, valeur_defaut=True).ajouter(params2) - Parametre("p_int", int, valeur_defaut=1).ajouter(params2) - Parametre("p_str2", str, valeur_defaut="toto").ajouter(params2) - importer(params, params2) - print "parametres apres:" - for param in params: - print params[param].debug() - print "" - print "" - params2 = {"p_bool":False, "p_int":17, "p_str":"titi"} - importer(params, params2) - print "parametres apres 2:" - for param in params: - print params[param].debug() - print "" - - print "-----------------------------" - print "test de fichier de config:" - cfg = ConfigParser.SafeConfigParser() - ecrire_config(params, cfg, "section1") - ecrire_fichier(cfg, "test.cfg") - Parametre("nouveau", str, valeur_defaut="nouveau parametre").ajouter(params) - print_fichier("test.cfg") - #params = {} - lire_config(params, "test.cfg", "section1") - for param in params: - print params[param].debug() - print "" - ecrire_config(params, cfg, "section1") - ecrire_fichier(cfg, "test.cfg") - print_fichier("test.cfg") - - - - +#!/usr/bin/python +# -*- coding: iso-8859-1 -*- +""" +Parametres - ExeFilter + +Le module Parametres permet de gérer les paramètres des différents modules +et filtres, grâce à la classe L{Parametres.Parametre}. + +Les paramètres d'un filtre doivent être regroupés dans un dictionnaire indexé +suivant les codes des paramètres, et manipulés grâce aux fonctions de ce module. + +Ce fichier fait partie du projet ExeFilter. +URL du projet: http://www.decalage.info/exefilter + +@organization: DGA/CELAR +@author: U{Philippe Lagadec} + +@contact: U{Philippe Lagadec} + +@copyright: DGA/CELAR 2004-2008 +@copyright: NATO/NC3A 2008 (modifications PL apres v1.1.0) + +@license: CeCILL (open-source compatible GPL) + cf. code source ou fichier LICENCE.txt joint + +@version: 1.02 + +@status: beta +""" +#============================================================================== +__docformat__ = 'epytext en' + +__date__ = "2008-03-24" +__version__ = "1.02" + +#------------------------------------------------------------------------------ +# LICENCE pour le projet ExeFilter: + +# Copyright DGA/CELAR 2004-2008 +# Copyright NATO/NC3A 2008 (PL changes after v1.1.0) +# Auteurs: +# - Philippe Lagadec (PL) - philippe.lagadec(a)laposte.net +# - Arnaud Kerréneur (AK) - arnaud.kerreneur(a)dga.defense.gouv.fr +# - Tanguy Vinceleux (TV) - tanguy.vinceleux(a)dga.defense.gouv.fr +# +# Ce logiciel est régi par la licence CeCILL soumise au droit français et +# respectant les principes de diffusion des logiciels libres. Vous pouvez +# utiliser, modifier et/ou redistribuer ce programme sous les conditions +# de la licence CeCILL telle que diffusée par le CEA, le CNRS et l'INRIA +# sur le site "http://www.cecill.info". Une copie de cette licence est jointe +# dans les fichiers Licence_CeCILL_V2-fr.html et Licence_CeCILL_V2-en.html. +# +# En contrepartie de l'accessibilité au code source et des droits de copie, +# de modification et de redistribution accordés par cette licence, il n'est +# offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, +# seule une responsabilité restreinte pèse sur l'auteur du programme, le +# titulaire des droits patrimoniaux et les concédants successifs. +# +# A cet égard l'attention de l'utilisateur est attirée sur les risques +# associés au chargement, à l'utilisation, à la modification et/ou au +# développement et à la reproduction du logiciel par l'utilisateur étant +# donné sa spécificité de logiciel libre, qui peut le rendre complexe à +# manipuler et qui le réserve donc à des développeurs et des professionnels +# avertis possédant des connaissances informatiques approfondies. Les +# utilisateurs sont donc invités à charger et tester l'adéquation du +# logiciel à leurs besoins dans des conditions permettant d'assurer la +# sécurité de leurs systèmes et ou de leurs données et, plus généralement, +# à l'utiliser et l'exploiter dans les mêmes conditions de sécurité. +# +# Le fait que vous puissiez accéder à cet en-tête signifie que vous avez +# pris connaissance de la licence CeCILL, et que vous en avez accepté les +# termes. + +#------------------------------------------------------------------------------ +# HISTORIQUE: +# 30/09/2005 v0.01 PL: - 1ère version +# 12/10/2005 v0.02 PL: - ajout et correction de quelques commentaires +# - modif complète des fonctions, pour permettre +# l'utilisation d'objets ConfigParser ou de fichiers +# - fonction ajouter_parametre transformée en méthode +# 12/01/2007 v1.00 PL: - version 1.00 officielle +# 2007-09-18 v1.01 PL: - ajout licence CeCILL +# 2008-03-27 v1.02 PL: - bug corrige dans Parametre.set + +#------------------------------------------------------------------------------ +# A FAIRE: +# + ajouter gestion de type=list, pour les listes d'extensions, avec conversion +# de chaînes en splittant suivant les virgules + strip espaces +# + ajouter attribut valeurs_possibles=None/liste, avec vérif dans set() +# + vérifier si p1=p2 fait bien une recopie de l'objet et non pas juste de la +# référence, sinon il faudra utiliser le module copy +# ? ajouter attribut valeurs_restrictives = "croissantes"/"decroissantes" ou +# True/False + méthodes pour pouvoir comparer + moyen pour fusionner en ne +# gardant que les params les + restrictifs ? +# - fonction reinit pour appliquer reinit sur chaque param d'un dico +# - mettre à jour docstring en utilisant mieux les fonctions d'epydoc, et +# déclarer les constantes. +#------------------------------------------------------------------------------ + +#=== IMPORTS ================================================================== +import sys +try: + import configparser as ConfigParser +except ImportError: + import ConfigParser +import os, copy + +#=== VARIABLES GLOBALES ======================================================= + + +#=== CONSTANTES =============================================================== + +__doc__ += """ +@var VALEURS_VRAIES: valeurs pouvant être utilisées pour un paramètre bool True +@var VALEURS_FAUSSES: valeurs pouvant être utilisées pour un paramètre bool False +""" +# valeurs pour convertir une chaîne en booléen (toujours en minuscules) +VALEURS_VRAIES = ["1", "oui", "o", "yes", "y", "vrai", "vraie", "v", "t", "true", "on"] +VALEURS_FAUSSES = ["0", "non", "n", "no", "faux", "fausse", "f", "false", "off"] + +#=== CLASSES ================================================================== + +#------------------------------------------------------------------------------ +# classe Parametre +#------------------- + +class Parametre: + """ + classe permettant de gérer un paramètre avec une valeur. + + @ivar code: nom court utilisé pour indexer un dictionnaire de paramètres, + et pour utiliser ce paramètre dans un fichier de configuration. + Doit être en minuscules, sans espaces et sans accents. + Exemple: "supprimer_macros" + @type code: str, unicode + + @ivar nom: Nom simple décrivant le paramètre, utilisable par exemple + dans une IHM, et comme commentaire dans un fichier de config. + Exemple: "Supprimer les macros" + @type nom: str, unicode + + @ivar description: (optionnel) Description plus longue et détaillée + du paramètre, utilisable comme aide contextuelle dans l'IHM et + comme commentaire dans un fichier de config. + Exemple: "Les macros peuvent contenir du code malveillant." + @type description: str, unicode + + @ivar type: classe Python de la valeur, par exemple bool, str, int, list, ... + @type type: classe Python + + @ivar valeur: valeur du paramètre, doit correspondre au type. + + @ivar valeur_defaut: valeur par défaut du paramètre + """ + + def __init__ (self, code, type, nom="", description="", valeur=None, + valeur_defaut=None): + """Constructeur d'objet Parametre. + (cf. classe Parametre pour la description des attributs) + + code et type sont obligatoires. + """ + self.code = code + self.nom = nom + self.description = description + self.type = type + # conversion de la valeur suivant le type, au cas où: + self.valeur_defaut = type(valeur_defaut) + # on doit initialiser la valeur à None, pour set(): + self.valeur = None + self.set(valeur) + +## def __set__(self, instance, value): +## """Pour fixer la valeur d'un paramètre, avec la syntaxe simple +## Parametre = valeur +## Cette méthode convertit automatiquement la valeur suivant le +## type déclaré du paramètre. +## """ +## self.valeur = self.type(value) + + def set(self, valeur): + """Pour fixer la valeur d'un paramètre. + Cette méthode convertit automatiquement la valeur suivant le + type déclaré du paramètre. + + - Si le type est bool et que la valeur est une chaîne les valeurs + spéciales suivantes sont prises en compte: + - True = 1, oui, O, yes, Y, T, True, on, ... + - False = 0, non, N, no, F, False, off, ... + - (cf. constantes L{Parametres.VALEURS_VRAIES} et + L{Parametres.VALEURS_FAUSSES}) + + - Si c'est un autre type (int, str, ...) la conversion automatique du + type est employée. + + - Si la valeur est None, la valeur par défaut est employée + si la valeur d'origine est aussi None (1ère initialisation) + (sinon pas de modification) + """ + # si aucune valeur fournie et qu'il n'y en avait pas déjà, + # on prend la valeur par défaut: + if valeur == None: + if self.valeur == None: + self.valeur = self.valeur_defaut + else: + # si la valeur est une chaîne (str ou unicode) et que le + # paramètre est un booléen, conversion: + if self.type == bool: + if isinstance(valeur, str) or isinstance(valeur, str): + if valeur.strip().lower() in VALEURS_VRAIES: + self.valeur = True + elif valeur.strip().lower() in VALEURS_FAUSSES: + self.valeur = False + else: + self.valeur = bool(valeur) + else: + # sinon on utilise la conversion automatique du type + # d'objet correspondant au paramètre: + self.valeur = self.type(valeur) + + + def ajouter (self, dico_parametres): + """Pour ajouter l'objet Parametre au dictionnaire dico_parametres. + + Exemple:: + Parametre("supprimer_macros", bool, nom="Supprimer les macros", + valeur_defaut=True).ajouter(Filtre_Word.parametres) + """ + # ATTENTION: ici on ne fait que recopier une référence à l'objet + # Parametre dans le dictionnaire. Si on l'ajoute à plusieurs dicos, + # l'objet sera partagé entre tous les dicos. + dico_parametres[self.code] = self + + + def reinit(self): + """Pour réinitialiser le paramètre à sa valeur par défaut. + """ + self.valeur = self.valeur_defaut + + + def __str__(self): + """Convertit la valeur du paramètre en chaîne, en 1 ou 0 si c'est + un bool. + + exemple d'utilisation: print str(parametre["activer_filtre"]) + """ + if self.type == bool: + if self.valeur: return "1" + else: return "0" + else: + return str(self.valeur) + + def debug(self): + """Retourne une chaîne décrivant complètement le paramètre + et ses attributs, utile surtout pour le débogage.""" + return "%s = %s\n" % (self.code, repr(self.valeur)) + \ + "- nom: %s\n" % self.nom + \ + "- description: %s\n" % self.description + \ + "- type: %s\n" % str(self.type) + \ + "- valeur par defaut: %s" % repr(self.valeur_defaut) + + +#=== FONCTIONS ================================================================ + +#------------------------------------------------------------------------------ +# importer +#--------------------- + +def importer (dico_parametres, nouveaux_parametres): + """Pour importer les valeurs du dictionnaire nouveaux_parametres + dans le dictionnaire dico_parametres. + nouveaux_parametres doit contenir des valeurs indexées par + les codes de paramètres, ou bien des objets Parametre. + """ + for code in nouveaux_parametres: + valeur = nouveaux_parametres[code] + if isinstance (valeur, Parametre): + # on utilise le module copy pour dupliquer le paramètre, car sinon + # on ne recopie qu'une référence au même paramètre: + dico_parametres[code] = copy.deepcopy(valeur) + else: + dico_parametres[code].set(valeur) + #Ancienne version, avec nouveaux_parametres un dico de Parametre + #dico_parametres.update(nouveaux_parametres) + +#------------------------------------------------------------------------------ +# lire_config +#--------------------- + +def lire_config (dico_parametres, config, section): + """Pour mettre à jour les valeurs du dictionnaire dico_parametres, + en lisant soit: + - un fichier de configuration au format INI, dans la section indiquée, + fourni sous forme d'objet file ou le nom du fichier + - un objet (Raw)ConfigParser + + Si un paramètre du fichier n'existe pas dans dico_parametres, il est + simplement ignoré. + + config : nom de fichier à lire, objet file, ou ConfigParser + section : chaîne + """ + if isinstance(config, ConfigParser.RawConfigParser): + # config est un objet (Raw)ConfigParser + cfg = config + else: + # on crée un ConfigParser vide: + cfg = ConfigParser.SafeConfigParser() + if isinstance(config, str) or isinstance(config, str): + # config est un nom de fichier + f = open(config, 'r') + cfg.readfp(f) + f.close() + elif hasattr(config, "read"): + # config est un fichier déjà ouvert + cfg.readfp(config) + else: + # sinon c'est une erreur + raise TypeError + # ensuite on peut exploiter la config lue, si la section demandée est + # présente: + if cfg.has_section(section): + for code, valeur in cfg.items(section): + if code in dico_parametres: + dico_parametres[code].set(valeur) + + +#------------------------------------------------------------------------------ +# ecrire_config +#--------------------- + +def ecrire_config (dico_parametres, config, section): + """Pour écrire les valeurs du dictionnaire dico_parametres dans + un objet ConfigParser, dans la section indiquée. + Si la section existait déjà, elle est simplement mise à jour. + + @param config: objet ConfigParser à écrire + @param section: chaîne + """ + if not config.has_section(section): + config.add_section(section) + for code in dico_parametres: + config.set(section, code, str(dico_parametres[code])) + +def lire_fichier(config, fichier): + """Pour lire un fichier dans un objet ConfigParser. + + @param config: objet ConfigParser à écrire + @param fichier: fichier à lire + @type fichier: nom de fichier ou objet file + """ + if hasattr(fichier, "read"): + # directement si c'est un objet file + config.readfp(fichier) + else: + # sinon on ouvre le fichier + f = open(fichier) + config.readfp(f) + f.close() + +def ecrire_fichier(config, fichier): + """Pour écrire un objet ConfigParser dans un fichier. + + @param config : objet ConfigParser à lire + @param fichier: fichier à écrire + @type fichier: nom de fichier ou objet file + """ + if hasattr(fichier, "read"): + # directement si c'est un objet file (par exemple sys.stdout) + config.write(fichier) + else: + # sinon on crée le fichier + f = open(fichier, "w") + config.write(f) + f.close() + + +#=== PROGRAMME PRINCIPAL (test) =============================================== + +if __name__ == "__main__": + print("-----------------------------") + print("TEST DU MODULE Parametres.py:") + print("-----------------------------") + print("") + + # fonction utiles uniquement pour les tests: + + def print_fichier(fichier): + """Pour afficher un fichier à l'écran.""" + f = open(fichier) + print(f.read()) + f.close() + + # BOOLEEN : + p1 = Parametre("p_bool", bool, nom="valeur booleenne", + description="Ceci est un parametre booleen.", + valeur_defaut=False) + print(p1.debug()) + p1.set(True) + print(p1.debug()) + p1.set("0") + print(p1.debug()) + print("test des valeurs bool:") + for valeur in VALEURS_VRAIES: + p1.set(valeur) + if p1.valeur == True: + print("%s: OK" % valeur) + else: + print("%s: NOK !" % valeur) + print(p1.debug()) + for valeur in VALEURS_FAUSSES: + p1.set(valeur) + if p1.valeur == False: + print("%s: OK" % valeur) + else: + print("%s: NOK !" % valeur) + print(p1.debug()) + print("-----------------------------") + + # ENTIER : + p = Parametre("p_entier", int, nom="valeur entiere", + description="Ceci est un parametre entier.", + valeur_defaut=0) + print(p.debug()) + p.set(17) + print(p.debug()) + p.set("254") + print(p.debug()) + p.set(True) + print(p.debug()) + print("-----------------------------") + + # CHAINE : + p = Parametre("p_chaine", str, nom="valeur chaine", + description="Ceci est un parametre chaine.", + valeur_defaut="(vide)") + print(p.debug()) + p.set("ceci est une chaine") + print(p.debug()) + p.set(1789) + print(p.debug()) + p.set(True) + print(p.debug()) + + print("-----------------------------") + print("test de dictionnaires de parametres:") + params = {} + Parametre("p_bool", bool, valeur_defaut=False).ajouter(params) + Parametre("p_int", int, valeur_defaut=0).ajouter(params) + Parametre("p_str", str, valeur_defaut="").ajouter(params) + print("parametres avant:") + for param in params: + print(params[param].debug()) + print("") + params2 = {} + Parametre("p_bool", bool, valeur_defaut=True).ajouter(params2) + Parametre("p_int", int, valeur_defaut=1).ajouter(params2) + Parametre("p_str2", str, valeur_defaut="toto").ajouter(params2) + importer(params, params2) + print("parametres apres:") + for param in params: + print(params[param].debug()) + print("") + print("") + params2 = {"p_bool":False, "p_int":17, "p_str":"titi"} + importer(params, params2) + print("parametres apres 2:") + for param in params: + print(params[param].debug()) + print("") + + print("-----------------------------") + print("test de fichier de config:") + cfg = ConfigParser.SafeConfigParser() + ecrire_config(params, cfg, "section1") + ecrire_fichier(cfg, "test.cfg") + Parametre("nouveau", str, valeur_defaut="nouveau parametre").ajouter(params) + print_fichier("test.cfg") + #params = {} + lire_config(params, "test.cfg", "section1") + for param in params: + print(params[param].debug()) + print("") + ecrire_config(params, cfg, "section1") + ecrire_fichier(cfg, "test.cfg") + print_fichier("test.cfg") + + + + diff --git a/Politique.py b/Politique.py index 080bbbb..3e74ce5 100644 --- a/Politique.py +++ b/Politique.py @@ -1,423 +1,426 @@ -#!/usr/bin/python -# -*- coding: iso-8859-1 -*- -""" -Politique - ExeFilter - -Le module Politique permet de charger, stocker et gérer des politiques de filtrage, -à l'aide de la classe L{Politique.Politique}. - -Ce fichier fait partie du projet ExeFilter. -URL du projet: http://www.decalage.info/exefilter - -@organization: DGA/CELAR -@author: U{Philippe Lagadec} -@author: U{Tanguy Vinceleux} - -@contact: U{Philippe Lagadec} - -@copyright: DGA/CELAR 2004-2007 -@license: CeCILL (open-source compatible GPL) - cf. code source ou fichier LICENCE.txt joint - -@version: 1.01 - -@status: beta -""" -__docformat__ = 'epytext en' - -#__author__ = "Philippe Lagadec, Tanguy Vinceleux (DGA/CELAR)" -__date__ = "2007-09-18" -__version__ = "1.01" - -#------------------------------------------------------------------------------ -# LICENCE pour le projet ExeFilter: - -# Copyright DGA/CELAR 2004-2007 -# Auteurs: -# - Philippe Lagadec (PL) - philippe.lagadec(a)laposte.net -# - Arnaud Kerréneur (AK) - arnaud.kerreneur(a)dga.defense.gouv.fr -# - Tanguy Vinceleux (TV) - tanguy.vinceleux(a)dga.defense.gouv.fr -# -# Ce logiciel est régi par la licence CeCILL soumise au droit français et -# respectant les principes de diffusion des logiciels libres. Vous pouvez -# utiliser, modifier et/ou redistribuer ce programme sous les conditions -# de la licence CeCILL telle que diffusée par le CEA, le CNRS et l'INRIA -# sur le site "http://www.cecill.info". Une copie de cette licence est jointe -# dans les fichiers Licence_CeCILL_V2-fr.html et Licence_CeCILL_V2-en.html. -# -# En contrepartie de l'accessibilité au code source et des droits de copie, -# de modification et de redistribution accordés par cette licence, il n'est -# offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, -# seule une responsabilité restreinte pèse sur l'auteur du programme, le -# titulaire des droits patrimoniaux et les concédants successifs. -# -# A cet égard l'attention de l'utilisateur est attirée sur les risques -# associés au chargement, à l'utilisation, à la modification et/ou au -# développement et à la reproduction du logiciel par l'utilisateur étant -# donné sa spécificité de logiciel libre, qui peut le rendre complexe à -# manipuler et qui le réserve donc à des développeurs et des professionnels -# avertis possédant des connaissances informatiques approfondies. Les -# utilisateurs sont donc invités à charger et tester l'adéquation du -# logiciel à leurs besoins dans des conditions permettant d'assurer la -# sécurité de leurs systèmes et ou de leurs données et, plus généralement, -# à l'utiliser et l'exploiter dans les mêmes conditions de sécurité. -# -# Le fait que vous puissiez accéder à cet en-tête signifie que vous avez -# pris connaissance de la licence CeCILL, et que vous en avez accepté les -# termes. - -#------------------------------------------------------------------------------ -# HISTORIQUE: -# 09/03/2005 v0.01 PL: - 1ère version, reprise du code charger_filtres de Sas.py -# 2005-2007 PL,TV: - nombreuses evolutions -# 12/01/2007 v1.00 PL: - version 1.00 officielle -# 2007-09-18 v1.01 PL: - ajout licence CeCILL -# 2007-10-08 PL: - tri des sections et parametres dans ecrire_html - -#------------------------------------------------------------------------------ -# A FAIRE: -# - sauver la politique dans le fichier indiqué, s'il n'existe pas ? -#------------------------------------------------------------------------------ - -#=== IMPORTS ================================================================== -import sys, codecs -import ConfigParser -import os - -# modules d'ExeFilter: -from commun import * -import Filtres, Parametres, ExeFilter - -# third party modules: -import thirdparty.HTML as HTML - -#=== VARIABLES GLOBALES ======================================================= - - -#=== CONSTANTES =============================================================== - -# section des paramètres globaux du logiciel dans les fichiers de config -SECTION_EXEFILTER = "ExeFilter" - - -#=== CLASSES ================================================================== - -#------------------------------------------------------------------------------ -# classe Politique -#------------------- - -class Politique: - """ - Classe permettant de manipuler une politique de filtrage, en y activant - les filtres voulus avec leurs paramètres, ainsi que certains paramètres - globaux du moteur de filtrage. - - @ivar filtres: liste d'objets L{Filtre} de la politique, - chacun possédant ses L{Parametres}. - @ivar parametres: dictionnaire des L{Parametres} globaux de la politique. - @ivar dico_filtres: dictionnaire des filtres, indexé suivant les extensions - de fichiers acceptées par chaque filtre. - """ - - def __init__(self, config=None, nom="sans nom"): - """ - Constructeur d'objet Politique. - - config peut être: - - un objet ConfigParser - - un nom de fichier conforme à la syntaxe ConfigParser (.INI) - - un objet file ouvert en lecture - - ou bien une liste d'objets d'un de ces 3 types - """ - self.nom = nom - # on commence par charger la liste des classes de filtres disponibles, - # en appelant la fonction du package Filtres qui va bien: - classes_filtres = Filtres.classes_filtres() - # on instancie chaque classe dans un objet filtre - self.filtres = [] - for filtre in classes_filtres: - self.filtres.append( filtre(self) ) - # les paramètres globaux d'ExeFilter sont vides par défaut: - self.parametres = {} - # on y importe les paramètres du module ExeFilter: - Parametres.importer(self.parametres, ExeFilter.parametres) - # ensuite on applique la config indiquée: - if config is not None: - self.lire_config(config) - else: - self.creer_dico_filtres() - - - def lire_config (self, config): - """Pour mettre à jour la configuration d'une politique. - - config peut être: - - un objet ConfigParser - - un nom de fichier conforme à la syntaxe ConfigParser (.INI) - - un objet file ouvert en lecture - - ou bien une liste d'objets d'un de ces 3 types - """ - if isinstance(config, list): - # si config est une liste, on la parcourt de façon récursive: - for item in config: - self.lire_config(item) - else: - # on parcourt les filtres, et on lit la section correspondant à - # chacun (section = nom du filtre), pour mettre à jour ses - # paramètres: - for filtre in self.filtres: - Parametres.lire_config(filtre.parametres, config, - filtre.nom_classe) - # puis on lit les paramètres globaux d'ExeFilter: - Parametres.lire_config(self.parametres, config, SECTION_EXEFILTER) - self.creer_dico_filtres() - - - def journaliser(self): - """Pour ajouter la liste des filtres employés au journal de débogage, - avec leur version.""" - Journal.info2("Politique: %s" % self.nom) - for filtre in self.filtres: - Journal.info2("- %s v%s du %s" % (filtre.nom_classe, filtre.version, - filtre.date)) - - def creer_dico_filtres(self): - """Pour créer un dictionnaire de filtres indexé suivant les extensions - de fichiers acceptées par chaque filtre. Ce dictionnaire doit être recréé - à chaque modification de la politique. Il est ensuite accessible en - tant qu'attribut dico_filtres. - - @return: dictionnaire de filtres - @rtype : dictionnaire - """ - self.dico_filtres = {} # dictionnaire des filtres de formats par extension - for filtre in self.filtres: - # on parcourt la liste des extensions de chaque filtre: - for ext in filtre.extensions: - # conversion en minuscules de l'extension, pour que la - # comparaison soit toujours correcte (cf. Fichier.py) - ext = ext.lower() - # on cherche s'il y a déjà des filtres avec cette extension - # dans le dictionnaire: - if ext in self.dico_filtres: - # si oui on ajoute le filtre à la liste correspondante - self.dico_filtres[ext].append(filtre) - else: - # si non on crée cette liste avec ce filtre et on - # l'ajoute au dictionnaire - self.dico_filtres[ext] = [filtre] - # on retourne le dictionnaire - return self.dico_filtres - - def ecrire_fichier (self, fichier, params_globaux=True, params_filtres=True, - selection_filtres=False, selection_parametres=False): - """Pour écrire la politique dans un fichier de configuration. - - @param fichier: fichier à écrire - @type fichier: nom de fichier ou objet file - - @param params_globaux: indique si les paramètres globaux doivent être - inclus (oui par défaut) - @type params_globaux: bool - @param params_filtres: indique si les paramètres des filtres doivent être - inclus (oui par défaut) - @type params_filtres: bool - @param selection_filtres: si ce paramètre contient une chaîne, seuls les - filtres avec ce paramètre ayant une valeur True seront inclus. - Si c'est un tuple, on vérifie la valeur indiquée, exemple: ("format_autorise", False). - (False par défaut) - @type selection_filtres: bool, str, tuple - @param selection_parametres: si ce paramètre contient une liste de - chaînes, seuls les paramètres des filtres dont le nom est listé seront - inclus, par exemple ["format_autorise"]. (False par défaut) - @type selection_parametres: bool, list - """ - # on crée d'abord un objet ConfigParser - cfg = ConfigParser.SafeConfigParser() - if params_globaux: - # on y stocke les paramètres globaux - Parametres.ecrire_config(self.parametres, cfg, SECTION_EXEFILTER) - if params_filtres: - # puis ceux de chaque filtre - for filtre in self.filtres: - inclure_filtre = True # par défaut on inclut tous les filtres - if selection_filtres: - inclure_filtre = False # ...sauf si sélection - # si on veut sélectionner les filtres suivant un paramètre, - # on doit d'abord vérifier que le filtre possède ce paramètre - if isinstance(selection_filtres, tuple): - # si c'est un tuple on vérifie le paramètre et sa valeur - nom_filtre = selection_filtres[0] - valeur = selection_filtres[1] - else: - # si c'est une chaîne on vérifie que le paramètre est vrai - nom_filtre = selection_filtres - valeur = True - if nom_filtre in filtre.parametres: - if filtre.parametres[nom_filtre].valeur == valeur: - inclure_filtre = True - if inclure_filtre: - if selection_parametres == False: - # on prend tous les paramètres du filtre - dico_params = filtre.parametres - else: - # sinon on sélectionne les paramètres indiqués - dico_params = {} - for param in selection_parametres: - if param in filtre.parametres: - dico_params[param] = filtre.parametres[param] - # puis on écrit le filtre dans la config, à la bonne section: - if len(dico_params)>0: - Parametres.ecrire_config(dico_params, cfg, filtre.nom_classe) - # pour finir on écrit le tout dans le fichier indiqué - Parametres.ecrire_fichier(cfg, fichier) - - def ecrire_html (self, nom_fichier_html): - """Pour écrire un fichier HTML décrivant en détails chaque paramètre de - la politique.""" - # table with 5 columns and header row - t = HTML.Table(header_row = (_('Code Paramètre'), _('Nom'), _('Description'), - _('Valeur'), _('Valeur par défaut'))) - # row for global section head: - t.rows.append(HTML.TableRow(('section [%s]' % SECTION_EXEFILTER, - '', '', '', ''), bgcolor='cyan')) - # fonction pour comparer 2 noms de parametres afin de trier la liste: - cmp_params = lambda p1,p2: cmp(p1.code.lower(), p2.code.lower()) - for p in sorted(self.parametres.itervalues(), cmp=cmp_params): - t.rows.append(('%s' % p.code, p.nom, p.description, str(p), str(p.valeur_defaut))) - # fonction pour comparer 2 noms de filtres afin de trier la liste: - cmp_filtres = lambda f1,f2: cmp(f1.nom_classe.lower(), f2.nom_classe.lower()) - for filtre in sorted(self.filtres, cmp=cmp_filtres): - # row for section head: - t.rows.append(HTML.TableRow(('section [%s]' % filtre.nom_classe, - '', '', '', ''), bgcolor='cyan')) - for p in sorted(filtre.parametres.itervalues(), cmp=cmp_params): - t.rows.append(('%s' % p.code, p.nom, p.description, str(p), str(p.valeur_defaut))) - f = open(nom_fichier_html, "w") - f.write('\n') - f.write(str(t)) - f.write('\n') - f.close() - -## f = codecs.open(nom_fichier_html, "w", "latin_1") -## f.write("") -## f.write('') -## f.write('') -## f.write(u'') -## f.write('') -## f.write('') -## f.write('') -## f.write(u'') -## f.write('') -## f.write('') -## f.write('' % SECTION_EXEFILTER) -## f.write('') -## # fonction pour comparer 2 noms de parametres afin de trier la liste: -## cmp_params = lambda p1,p2: cmp(p1.code.lower(), p2.code.lower()) -## for p in sorted(self.parametres.itervalues(), cmp=cmp_params): -## f.write('') -## f.write(u'' % p.code) -## f.write(u'' % unistr(p.nom)) -## f.write(u'' % unistr(p.description)) -## f.write(u'' % unistr(str(p))) -## f.write(u'' % unistr(str(p.valeur_defaut))) -## f.write('') -## # fonction pour comparer 2 noms de filtres afin de trier la liste: -## cmp_filtres = lambda f1,f2: cmp(f1.nom_classe.lower(), f2.nom_classe.lower()) -## for filtre in sorted(self.filtres, cmp=cmp_filtres): -## f.write('') -## f.write('' % filtre.nom_classe) -## f.write('') -## for p in sorted(filtre.parametres.itervalues(), cmp=cmp_params): -## f.write('') -## f.write(u'' % p.code) -## f.write(u'' % unistr(p.nom)) -## f.write(u'' % unistr(p.description)) -## f.write(u'' % unistr(str(p))) -## f.write(u'' % unistr(str(p.valeur_defaut))) -## f.write('') -## f.write('
Code Paramètre
Nom
Description
Valeur
Valeur par défaut
section [%s]
%s%s%s%s%s
section [%s]
%s%s%s%s%s
') -## f.write("") -## f.close() - - -#=== PROGRAMME PRINCIPAL (test) =============================================== - -if __name__ == "__main__": - print "-----------------------------" - print "TEST DU MODULE Politique.py:" - print "-----------------------------" - print "" - - print "creation d'une config de test avec rep_temp=TEST." - c = ConfigParser.SafeConfigParser() - c.add_section(SECTION_EXEFILTER) - c.set(SECTION_EXEFILTER, "rep_temp", "TEST") - - print "creation d'une politique p avec cette config." - p = Politique() - p.ecrire_html("Politique.html") - p.lire_config(c) - - print "ecriture de cette politique p dans un fichier:" - p.ecrire_fichier("test_Politique.cfg") - print "-------------------------------------------------" - import sys - p.ecrire_fichier(sys.stdout) - print "-------------------------------------------------" - - print "creation d'une politique p2 en lisant le fichier de config." - p2 = Politique("test_Politique.cfg") - if p2.parametres['rep_temp'].valeur == 'TEST': - print "OK, le parametre modifie a ete pris en compte." - else: - print "NOK, le parametre modifie n'a pas ete pris en compte !" - - print "creation d'une politique p3 sans lire le fichier de config." - p3 = Politique() - p3.ecrire_fichier(sys.stdout) - if p3.parametres['rep_temp'].valeur == 'TEST': - print "NOK, le parametre modifie dans p2 a ete conserve !" - else: - print "OK, on a bien tous les parametres par defaut." - - print "modif de la politique p3 pour ajouter parametre specifique_utilisateur" - print "a certains filtres." - for filtre in p3.filtres: - # on sélectionne les filtres Microsoft: - if "MS" in filtre.nom: - p = Parametres.Parametre("specifique_utilisateur", bool, valeur_defaut=True) - p.ajouter(filtre.parametres) - print "Parametres globaux uniquement:" - print "-------------------------------------------------" - p3.ecrire_fichier(sys.stdout, params_filtres=False) - print "-------------------------------------------------" - print "Parametres filtres uniquement:" - print "-------------------------------------------------" - p3.ecrire_fichier(sys.stdout, params_globaux=False) - print "-------------------------------------------------" - print "Parametres filtres specifique_utilisateur uniquement:" - print "-------------------------------------------------" - p3.ecrire_fichier(sys.stdout, params_globaux=False, - selection_filtres="specifique_utilisateur") - print "-------------------------------------------------" - print "Parametres tous filtres format_autorise uniquement:" - print "-------------------------------------------------" - p3.ecrire_fichier(sys.stdout, params_globaux=False, - selection_parametres=["format_autorise"]) - print "---------------------------------------------------------" - print "Parametres filtres format_autorise=False uniquement:" - print "---------------------------------------------------------" - # on interdit les filtres PDF et JPEG - for filtre in p3.filtres: - if filtre.nom in ["Document PDF", "Fichier Image JPEG"]: - filtre.parametres["format_autorise"].set(False) - # ensuite on ne veut que ces filtres interdits dans la config: - p3.ecrire_fichier(sys.stdout, params_globaux=False, - selection_filtres=("format_autorise", False), - selection_parametres=["format_autorise"]) - print "-------------------------------------------------" - - import os - os.remove("test_Politique.cfg") - - +#!/usr/bin/python +# -*- coding: iso-8859-1 -*- +""" +Politique - ExeFilter + +Le module Politique permet de charger, stocker et gérer des politiques de filtrage, +à l'aide de la classe L{Politique.Politique}. + +Ce fichier fait partie du projet ExeFilter. +URL du projet: http://www.decalage.info/exefilter + +@organization: DGA/CELAR +@author: U{Philippe Lagadec} +@author: U{Tanguy Vinceleux} + +@contact: U{Philippe Lagadec} + +@copyright: DGA/CELAR 2004-2007 +@license: CeCILL (open-source compatible GPL) - cf. code source ou fichier LICENCE.txt joint + +@version: 1.01 + +@status: beta +""" +__docformat__ = 'epytext en' + +#__author__ = "Philippe Lagadec, Tanguy Vinceleux (DGA/CELAR)" +__date__ = "2007-09-18" +__version__ = "1.01" + +#------------------------------------------------------------------------------ +# LICENCE pour le projet ExeFilter: + +# Copyright DGA/CELAR 2004-2007 +# Auteurs: +# - Philippe Lagadec (PL) - philippe.lagadec(a)laposte.net +# - Arnaud Kerréneur (AK) - arnaud.kerreneur(a)dga.defense.gouv.fr +# - Tanguy Vinceleux (TV) - tanguy.vinceleux(a)dga.defense.gouv.fr +# +# Ce logiciel est régi par la licence CeCILL soumise au droit français et +# respectant les principes de diffusion des logiciels libres. Vous pouvez +# utiliser, modifier et/ou redistribuer ce programme sous les conditions +# de la licence CeCILL telle que diffusée par le CEA, le CNRS et l'INRIA +# sur le site "http://www.cecill.info". Une copie de cette licence est jointe +# dans les fichiers Licence_CeCILL_V2-fr.html et Licence_CeCILL_V2-en.html. +# +# En contrepartie de l'accessibilité au code source et des droits de copie, +# de modification et de redistribution accordés par cette licence, il n'est +# offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, +# seule une responsabilité restreinte pèse sur l'auteur du programme, le +# titulaire des droits patrimoniaux et les concédants successifs. +# +# A cet égard l'attention de l'utilisateur est attirée sur les risques +# associés au chargement, à l'utilisation, à la modification et/ou au +# développement et à la reproduction du logiciel par l'utilisateur étant +# donné sa spécificité de logiciel libre, qui peut le rendre complexe à +# manipuler et qui le réserve donc à des développeurs et des professionnels +# avertis possédant des connaissances informatiques approfondies. Les +# utilisateurs sont donc invités à charger et tester l'adéquation du +# logiciel à leurs besoins dans des conditions permettant d'assurer la +# sécurité de leurs systèmes et ou de leurs données et, plus généralement, +# à l'utiliser et l'exploiter dans les mêmes conditions de sécurité. +# +# Le fait que vous puissiez accéder à cet en-tête signifie que vous avez +# pris connaissance de la licence CeCILL, et que vous en avez accepté les +# termes. + +#------------------------------------------------------------------------------ +# HISTORIQUE: +# 09/03/2005 v0.01 PL: - 1ère version, reprise du code charger_filtres de Sas.py +# 2005-2007 PL,TV: - nombreuses evolutions +# 12/01/2007 v1.00 PL: - version 1.00 officielle +# 2007-09-18 v1.01 PL: - ajout licence CeCILL +# 2007-10-08 PL: - tri des sections et parametres dans ecrire_html + +#------------------------------------------------------------------------------ +# A FAIRE: +# - sauver la politique dans le fichier indiqué, s'il n'existe pas ? +#------------------------------------------------------------------------------ + +#=== IMPORTS ================================================================== +import sys, codecs +try: + import configparser as ConfigParser +except ImportError: + import ConfigParser +import os + +# modules d'ExeFilter: +from commun import * +import Filtres, Parametres, ExeFilter + +# third party modules: +import thirdparty.HTML as HTML + +#=== VARIABLES GLOBALES ======================================================= + + +#=== CONSTANTES =============================================================== + +# section des paramètres globaux du logiciel dans les fichiers de config +SECTION_EXEFILTER = "ExeFilter" + + +#=== CLASSES ================================================================== + +#------------------------------------------------------------------------------ +# classe Politique +#------------------- + +class Politique: + """ + Classe permettant de manipuler une politique de filtrage, en y activant + les filtres voulus avec leurs paramètres, ainsi que certains paramètres + globaux du moteur de filtrage. + + @ivar filtres: liste d'objets L{Filtre} de la politique, + chacun possédant ses L{Parametres}. + @ivar parametres: dictionnaire des L{Parametres} globaux de la politique. + @ivar dico_filtres: dictionnaire des filtres, indexé suivant les extensions + de fichiers acceptées par chaque filtre. + """ + + def __init__(self, config=None, nom="sans nom"): + """ + Constructeur d'objet Politique. + + config peut être: + - un objet ConfigParser + - un nom de fichier conforme à la syntaxe ConfigParser (.INI) + - un objet file ouvert en lecture + - ou bien une liste d'objets d'un de ces 3 types + """ + self.nom = nom + # on commence par charger la liste des classes de filtres disponibles, + # en appelant la fonction du package Filtres qui va bien: + classes_filtres = Filtres.classes_filtres() + # on instancie chaque classe dans un objet filtre + self.filtres = [] + for filtre in classes_filtres: + self.filtres.append( filtre(self) ) + # les paramètres globaux d'ExeFilter sont vides par défaut: + self.parametres = {} + # on y importe les paramètres du module ExeFilter: + Parametres.importer(self.parametres, ExeFilter.parametres) + # ensuite on applique la config indiquée: + if config is not None: + self.lire_config(config) + else: + self.creer_dico_filtres() + + + def lire_config (self, config): + """Pour mettre à jour la configuration d'une politique. + + config peut être: + - un objet ConfigParser + - un nom de fichier conforme à la syntaxe ConfigParser (.INI) + - un objet file ouvert en lecture + - ou bien une liste d'objets d'un de ces 3 types + """ + if isinstance(config, list): + # si config est une liste, on la parcourt de façon récursive: + for item in config: + self.lire_config(item) + else: + # on parcourt les filtres, et on lit la section correspondant à + # chacun (section = nom du filtre), pour mettre à jour ses + # paramètres: + for filtre in self.filtres: + Parametres.lire_config(filtre.parametres, config, + filtre.nom_classe) + # puis on lit les paramètres globaux d'ExeFilter: + Parametres.lire_config(self.parametres, config, SECTION_EXEFILTER) + self.creer_dico_filtres() + + + def journaliser(self): + """Pour ajouter la liste des filtres employés au journal de débogage, + avec leur version.""" + Journal.info2("Politique: %s" % self.nom) + for filtre in self.filtres: + Journal.info2("- %s v%s du %s" % (filtre.nom_classe, filtre.version, + filtre.date)) + + def creer_dico_filtres(self): + """Pour créer un dictionnaire de filtres indexé suivant les extensions + de fichiers acceptées par chaque filtre. Ce dictionnaire doit être recréé + à chaque modification de la politique. Il est ensuite accessible en + tant qu'attribut dico_filtres. + + @return: dictionnaire de filtres + @rtype : dictionnaire + """ + self.dico_filtres = {} # dictionnaire des filtres de formats par extension + for filtre in self.filtres: + # on parcourt la liste des extensions de chaque filtre: + for ext in filtre.extensions: + # conversion en minuscules de l'extension, pour que la + # comparaison soit toujours correcte (cf. Fichier.py) + ext = ext.lower() + # on cherche s'il y a déjà des filtres avec cette extension + # dans le dictionnaire: + if ext in self.dico_filtres: + # si oui on ajoute le filtre à la liste correspondante + self.dico_filtres[ext].append(filtre) + else: + # si non on crée cette liste avec ce filtre et on + # l'ajoute au dictionnaire + self.dico_filtres[ext] = [filtre] + # on retourne le dictionnaire + return self.dico_filtres + + def ecrire_fichier (self, fichier, params_globaux=True, params_filtres=True, + selection_filtres=False, selection_parametres=False): + """Pour écrire la politique dans un fichier de configuration. + + @param fichier: fichier à écrire + @type fichier: nom de fichier ou objet file + + @param params_globaux: indique si les paramètres globaux doivent être + inclus (oui par défaut) + @type params_globaux: bool + @param params_filtres: indique si les paramètres des filtres doivent être + inclus (oui par défaut) + @type params_filtres: bool + @param selection_filtres: si ce paramètre contient une chaîne, seuls les + filtres avec ce paramètre ayant une valeur True seront inclus. + Si c'est un tuple, on vérifie la valeur indiquée, exemple: ("format_autorise", False). + (False par défaut) + @type selection_filtres: bool, str, tuple + @param selection_parametres: si ce paramètre contient une liste de + chaînes, seuls les paramètres des filtres dont le nom est listé seront + inclus, par exemple ["format_autorise"]. (False par défaut) + @type selection_parametres: bool, list + """ + # on crée d'abord un objet ConfigParser + cfg = ConfigParser.SafeConfigParser() + if params_globaux: + # on y stocke les paramètres globaux + Parametres.ecrire_config(self.parametres, cfg, SECTION_EXEFILTER) + if params_filtres: + # puis ceux de chaque filtre + for filtre in self.filtres: + inclure_filtre = True # par défaut on inclut tous les filtres + if selection_filtres: + inclure_filtre = False # ...sauf si sélection + # si on veut sélectionner les filtres suivant un paramètre, + # on doit d'abord vérifier que le filtre possède ce paramètre + if isinstance(selection_filtres, tuple): + # si c'est un tuple on vérifie le paramètre et sa valeur + nom_filtre = selection_filtres[0] + valeur = selection_filtres[1] + else: + # si c'est une chaîne on vérifie que le paramètre est vrai + nom_filtre = selection_filtres + valeur = True + if nom_filtre in filtre.parametres: + if filtre.parametres[nom_filtre].valeur == valeur: + inclure_filtre = True + if inclure_filtre: + if selection_parametres == False: + # on prend tous les paramètres du filtre + dico_params = filtre.parametres + else: + # sinon on sélectionne les paramètres indiqués + dico_params = {} + for param in selection_parametres: + if param in filtre.parametres: + dico_params[param] = filtre.parametres[param] + # puis on écrit le filtre dans la config, à la bonne section: + if len(dico_params)>0: + Parametres.ecrire_config(dico_params, cfg, filtre.nom_classe) + # pour finir on écrit le tout dans le fichier indiqué + Parametres.ecrire_fichier(cfg, fichier) + + def ecrire_html (self, nom_fichier_html): + """Pour écrire un fichier HTML décrivant en détails chaque paramètre de + la politique.""" + # table with 5 columns and header row + t = HTML.Table(header_row = (_('Code Paramètre'), _('Nom'), _('Description'), + _('Valeur'), _('Valeur par défaut'))) + # row for global section head: + t.rows.append(HTML.TableRow(('section [%s]' % SECTION_EXEFILTER, + '', '', '', ''), bgcolor='cyan')) + # fonction pour comparer 2 noms de parametres afin de trier la liste: + cmp_params = lambda p1,p2: cmp(p1.code.lower(), p2.code.lower()) + for p in sorted(self.parametres.values(), cmp=cmp_params): + t.rows.append(('%s' % p.code, p.nom, p.description, str(p), str(p.valeur_defaut))) + # fonction pour comparer 2 noms de filtres afin de trier la liste: + cmp_filtres = lambda f1,f2: cmp(f1.nom_classe.lower(), f2.nom_classe.lower()) + for filtre in sorted(self.filtres, cmp=cmp_filtres): + # row for section head: + t.rows.append(HTML.TableRow(('section [%s]' % filtre.nom_classe, + '', '', '', ''), bgcolor='cyan')) + for p in sorted(filtre.parametres.values(), cmp=cmp_params): + t.rows.append(('%s' % p.code, p.nom, p.description, str(p), str(p.valeur_defaut))) + f = open(nom_fichier_html, "w") + f.write('\n') + f.write(str(t)) + f.write('\n') + f.close() + +## f = codecs.open(nom_fichier_html, "w", "latin_1") +## f.write("") +## f.write('') +## f.write('') +## f.write(u'') +## f.write('') +## f.write('') +## f.write('') +## f.write(u'') +## f.write('') +## f.write('') +## f.write('' % SECTION_EXEFILTER) +## f.write('') +## # fonction pour comparer 2 noms de parametres afin de trier la liste: +## cmp_params = lambda p1,p2: cmp(p1.code.lower(), p2.code.lower()) +## for p in sorted(self.parametres.values(), cmp=cmp_params): +## f.write('') +## f.write(u'' % p.code) +## f.write(u'' % unistr(p.nom)) +## f.write(u'' % unistr(p.description)) +## f.write(u'' % unistr(str(p))) +## f.write(u'' % unistr(str(p.valeur_defaut))) +## f.write('') +## # fonction pour comparer 2 noms de filtres afin de trier la liste: +## cmp_filtres = lambda f1,f2: cmp(f1.nom_classe.lower(), f2.nom_classe.lower()) +## for filtre in sorted(self.filtres, cmp=cmp_filtres): +## f.write('') +## f.write('' % filtre.nom_classe) +## f.write('') +## for p in sorted(filtre.parametres.values(), cmp=cmp_params): +## f.write('') +## f.write(u'' % p.code) +## f.write(u'' % unistr(p.nom)) +## f.write(u'' % unistr(p.description)) +## f.write(u'' % unistr(str(p))) +## f.write(u'' % unistr(str(p.valeur_defaut))) +## f.write('') +## f.write('
Code Paramètre
Nom
Description
Valeur
Valeur par défaut
section [%s]
%s%s%s%s%s
section [%s]
%s%s%s%s%s
') +## f.write("") +## f.close() + + +#=== PROGRAMME PRINCIPAL (test) =============================================== + +if __name__ == "__main__": + print("-----------------------------") + print("TEST DU MODULE Politique.py:") + print("-----------------------------") + print("") + + print("creation d'une config de test avec rep_temp=TEST.") + c = ConfigParser.SafeConfigParser() + c.add_section(SECTION_EXEFILTER) + c.set(SECTION_EXEFILTER, "rep_temp", "TEST") + + print("creation d'une politique p avec cette config.") + p = Politique() + p.ecrire_html("Politique.html") + p.lire_config(c) + + print("ecriture de cette politique p dans un fichier:") + p.ecrire_fichier("test_Politique.cfg") + print("-------------------------------------------------") + import sys + p.ecrire_fichier(sys.stdout) + print("-------------------------------------------------") + + print("creation d'une politique p2 en lisant le fichier de config.") + p2 = Politique("test_Politique.cfg") + if p2.parametres['rep_temp'].valeur == 'TEST': + print("OK, le parametre modifie a ete pris en compte.") + else: + print("NOK, le parametre modifie n'a pas ete pris en compte !") + + print("creation d'une politique p3 sans lire le fichier de config.") + p3 = Politique() + p3.ecrire_fichier(sys.stdout) + if p3.parametres['rep_temp'].valeur == 'TEST': + print("NOK, le parametre modifie dans p2 a ete conserve !") + else: + print("OK, on a bien tous les parametres par defaut.") + + print("modif de la politique p3 pour ajouter parametre specifique_utilisateur") + print("a certains filtres.") + for filtre in p3.filtres: + # on sélectionne les filtres Microsoft: + if "MS" in filtre.nom: + p = Parametres.Parametre("specifique_utilisateur", bool, valeur_defaut=True) + p.ajouter(filtre.parametres) + print("Parametres globaux uniquement:") + print("-------------------------------------------------") + p3.ecrire_fichier(sys.stdout, params_filtres=False) + print("-------------------------------------------------") + print("Parametres filtres uniquement:") + print("-------------------------------------------------") + p3.ecrire_fichier(sys.stdout, params_globaux=False) + print("-------------------------------------------------") + print("Parametres filtres specifique_utilisateur uniquement:") + print("-------------------------------------------------") + p3.ecrire_fichier(sys.stdout, params_globaux=False, + selection_filtres="specifique_utilisateur") + print("-------------------------------------------------") + print("Parametres tous filtres format_autorise uniquement:") + print("-------------------------------------------------") + p3.ecrire_fichier(sys.stdout, params_globaux=False, + selection_parametres=["format_autorise"]) + print("---------------------------------------------------------") + print("Parametres filtres format_autorise=False uniquement:") + print("---------------------------------------------------------") + # on interdit les filtres PDF et JPEG + for filtre in p3.filtres: + if filtre.nom in ["Document PDF", "Fichier Image JPEG"]: + filtre.parametres["format_autorise"].set(False) + # ensuite on ne veut que ces filtres interdits dans la config: + p3.ecrire_fichier(sys.stdout, params_globaux=False, + selection_filtres=("format_autorise", False), + selection_parametres=["format_autorise"]) + print("-------------------------------------------------") + + import os + os.remove("test_Politique.cfg") + + diff --git a/Rapport.py b/Rapport.py index ed6c333..973fd38 100644 --- a/Rapport.py +++ b/Rapport.py @@ -1,363 +1,363 @@ -#!/usr/bin/python -# -*- coding: iso-8859-1 -*- -""" -Rapport - ExeFilter - -Module pour générer des rapports aux formats HTML et XML après un transfert. - -Ce fichier fait partie du projet ExeFilter. -URL du projet: http://www.decalage.info/exefilter - -@organization: DGA/CELAR -@author: U{Philippe Lagadec} -@author: U{Arnaud Kerréneur} - -@contact: U{Philippe Lagadec} - -@copyright: DGA/CELAR 2004-2008 -@copyright: NATO/NC3A 2008-2010 (modifications PL apres ExeFilter v1.1.0) - -@license: CeCILL (open-source compatible GPL) - cf. code source ou fichier LICENCE.txt joint - -@version: 1.05 - -@status: beta -""" -__docformat__ = 'epytext en' - -__date__ = "2011-05-01" -__version__ = "1.05" - -#------------------------------------------------------------------------------ -# LICENCE pour le projet ExeFilter: - -# Copyright DGA/CELAR 2004-2008 -# Copyright NATO/NC3A 2008-2010 (PL changes after ExeFilter v1.1.0) -# Auteurs: -# - Philippe Lagadec (PL) - philippe.lagadec(a)laposte.net -# - Arnaud Kerréneur (AK) - arnaud.kerreneur(a)dga.defense.gouv.fr -# - Tanguy Vinceleux (TV) - tanguy.vinceleux(a)dga.defense.gouv.fr -# -# Ce logiciel est régi par la licence CeCILL soumise au droit français et -# respectant les principes de diffusion des logiciels libres. Vous pouvez -# utiliser, modifier et/ou redistribuer ce programme sous les conditions -# de la licence CeCILL telle que diffusée par le CEA, le CNRS et l'INRIA -# sur le site "http://www.cecill.info". Une copie de cette licence est jointe -# dans les fichiers Licence_CeCILL_V2-fr.html et Licence_CeCILL_V2-en.html. -# -# En contrepartie de l'accessibilité au code source et des droits de copie, -# de modification et de redistribution accordés par cette licence, il n'est -# offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, -# seule une responsabilité restreinte pèse sur l'auteur du programme, le -# titulaire des droits patrimoniaux et les concédants successifs. -# -# A cet égard l'attention de l'utilisateur est attirée sur les risques -# associés au chargement, à l'utilisation, à la modification et/ou au -# développement et à la reproduction du logiciel par l'utilisateur étant -# donné sa spécificité de logiciel libre, qui peut le rendre complexe à -# manipuler et qui le réserve donc à des développeurs et des professionnels -# avertis possédant des connaissances informatiques approfondies. Les -# utilisateurs sont donc invités à charger et tester l'adéquation du -# logiciel à leurs besoins dans des conditions permettant d'assurer la -# sécurité de leurs systèmes et ou de leurs données et, plus généralement, -# à l'utiliser et l'exploiter dans les mêmes conditions de sécurité. -# -# Le fait que vous puissiez accéder à cet en-tête signifie que vous avez -# pris connaissance de la licence CeCILL, et que vous en avez accepté les -# termes. - -#------------------------------------------------------------------------------ -# HISTORIQUE: -# 14/01/2005 v0.01 AK: - 1ère version -# 2005-2007 PL,AK: - nombreuses evolutions -# 12/01/2007 v1.00 PL: - version 1.00 officielle -# 2007-09-18 v1.01 PL: - ajout licence CeCILL -# - ajout get_username pour améliorer la portabilité -# - renommage de parametres generer_rapport -# - retrait date de la version ExeFilter des rapports -# 2008-03-23 v1.02 PL: - ajout _() a chaque constante chaine pour traduction -# 2010-02-07 v1.03 PL: - removed path module import -# 2010-02-09 v1.04 PL: - workaround when username cannot be determined -# 2011-05-01 v1.05 PL: - fix when destination dir is None -# - added initial support for scan mode - -#------------------------------------------------------------------------------ -# A FAIRE: - -# EVOLUTIONS ENVISAGEES: -# + generer_rapport(): utiliser politique plutôt que nom_rapport ? -# + pour l'affichage du répertoire source, afficher le path complet (avec lettre de volume) -# ? générer un rapport au format Excel (l'IHM laisserait le choix entre html et xls) ? -# - paramètres pour choisir quels types de rapport générer (d'après politique) -# ? l'ouverture automatique du rapport HTML devrait être un paramètre d'ExeFilter -# - séparer les rapports HTML et XML dans 2 fonctions -# - utiliser cElementTree pour simplifier la génération de code XML (voire XHTML?) -#------------------------------------------------------------------------------ - -#=== IMPORTS ================================================================== - -import sys, os, socket, codecs, xml.sax.saxutils, os.path, time, locale - -# modules du projet: -import commun -from commun import * -import Resultat - -#=== CONSTANTES =============================================================== - -#=== VARIABLES GLOBALES ======================================================= - -# liste des objets Resultats qui vont servir à générer le rapport final: -# (vide au départ) -#TODO: deplacer cette liste dans l'objet ExeFilter -liste_resultats = [] - -#------------------------------------------------------------------------------ -# AJOUTER_RESULTAT -#------------------- - -def ajouter_resultat (resultat): - """Pour ajouter un résultat à la liste des résultats, qui sert ensuite à - produire un rapport global.""" - # liste_resultats est une variable globale, il faut le préciser: - global liste_resultats - liste_resultats.append(resultat) - -#------------------------------------------------------------------------------ -# ECHAP -#------------------- - -def echap (chaine): - """ - Pour remplacer tous les caractères ayant une signification en HTML - comme '<', '>' ou '&' par un encodage correct: '<', '>', '&', ... - """ - if chaine: - return xml.sax.saxutils.escape(chaine) - else: - return '' - -#------------------------------------------------------------------------------ -# GENERER_RAPPORT -#------------------- - -def generer_rapport(nom_rapport, repertoire_src, repertoire_dest, version, - date_version, continuer_transfert): - """ - Génère un rapport au format HTML et XML. - - Retourne le résumé de l'analyse dans une liste. - - @param repertoire_src : chemin du répertoire analysé - @param nom_rapport : nom des fichiers rapports HTML/XML (sans extension) - @param version : numéro de version du logiciel - @param date_version : date de version du logiciel - @param continuer_transfert: booléen pour savoir s'il y a eu interruption de l'analyse - """ - # initialisation des variables compteurs (nb fichiers analysés, - # nb fichiers acceptés, etc.) - nb_fichier = 0 - nb_accepte = 0 - nb_nettoye = 0 - nb_refuse = 0 - nb_erreur = 0 - - # création du répertoire des rapports s'ils n'existe pas: - #p = path(politique.parametres['rep_rapports'].valeur) - p = os.path.dirname(nom_rapport) - if not os.path.exists(p): - #p.makedirs() - os.makedirs(p) - - # on incrémente les variables compteurs - for resultat in liste_resultats: - nb_fichier = nb_fichier+1 - if resultat.code_resultat == Resultat.ACCEPTE: - nb_accepte = nb_accepte+1 - elif resultat.code_resultat == Resultat.NETTOYE: - nb_nettoye = nb_nettoye+1 - elif resultat.code_resultat == Resultat.REFUSE \ - or resultat.code_resultat == Resultat.EXT_NON_AUTORISEE \ - or resultat.code_resultat == Resultat.FORMAT_INCORRECT \ - or resultat.code_resultat == Resultat.VIRUS : - nb_refuse = nb_refuse+1 - elif resultat.code_resultat == Resultat.ERREUR_LECTURE \ - or resultat.code_resultat == Resultat.ERREUR_ANALYSE: - nb_erreur = nb_erreur+1 - - # on récupère le nom de l'utilisateur qui lance ExeFilter, avec nom de - # domaine (ou de machine) sous Windows: - try: - username_withdomain = get_username(with_domain=True) - except: - # workaround if user name cannot be determined - username_withdomain = 'unknown' - hostname = socket.gethostname() - - # création du rapport HTML "nom_rapport", ouverture en écriture - f=codecs.open(nom_rapport + ".html", 'w', 'latin_1') - # le rapport est un fichier encodé en Latin-1, pour éviter les erreurs unicode: - #f = codecs.EncodedFile (f1, 'latin_1', 'latin_1') - f.write(u'\n') - f.write(u'\n') - f.write(u'\n') - f.write(_(u'Rapport ExeFilter\n')) - f.write(u'\n') - f.write(u'\n\n') - f.write(u'\n') - - # écriture du bandeau titre - f.write(u'
\n') - f.write(_(u'Rapport ExeFilter v%(version)s') % {'version': version} + '\n
') - date = unicode(time.strftime("%c", time.localtime()), 'latin_1') - f.write(_(u"généré le %(date)s sur la machine %(hostname)s par l'utilisateur %(user)s")\ - % {'date':date, 'hostname':hostname, 'user':username_withdomain}) - f.write("\n
") - if repertoire_dest: - f.write(_(u'Répertoire de destination : ') + echap(repertoire_dest)) - f.write('\n
') - f.write(_(u'Répertoire(s) et/ou fichier(s) analysé(s) : ') + echap(repertoire_src)) - f.write(u'\n

\n\n') - - # affichage d'un message particulier s'il y a eu une interruption pendant l'analyse - if continuer_transfert == False: - f.write(u'
') - f.write(_(u"ATTENTION ! L'analyse a été interrompue pendant son exécution : ")) - f.write(_(u"tous les fichiers n'ont pas été analysés.")) - f.write(u'
') - - # écriture du résumé (nb fichiers analysés, nb fichiers acceptés, etc.) - f.write('
'+_(u'RESUME :')+'
\n') - f.write(_(u'Nombre de fichiers analysés : ') + str(nb_fichier) + '
\n') - if commun.clean_mode: - f.write(_(u'Nombre de fichiers acceptés : ') + str(nb_accepte) + '
\n') - f.write(_(u'Nombre de fichiers nettoyés : ') + str(nb_nettoye) + '
\n') - f.write(_(u'Nombre de fichiers refusés : ') + str(nb_refuse) + '
\n') - else: - f.write(u'Number of clean files : ' + str(nb_accepte) + '
\n') - f.write(u'Number of files to be cleaned: ' + str(nb_nettoye) + '
\n') - f.write(u'Number of not allowed files : ' + str(nb_refuse) + '
\n') - f.write(_(u"Nombre d'erreurs : ") + str(nb_erreur) + '
\n\n') - - # écriture du tableau contenant les fichiers analysés - f.write(u'
\n') - f.write(u'\n') - f.write(u'\n') - f.write(u'\n') - f.write(u'\n\n') - - if commun.clean_mode: - MSG_ACCEPTED = _(u'Accepté') - MSG_CLEANED = _(u'Nettoyé') - MSG_BLOCKED = _(u'Refusé') - MSG_ERROR = _(u'Erreur') - else: - MSG_ACCEPTED = u'Clean' - MSG_CLEANED = u'To be cleaned' - MSG_BLOCKED = u'Not allowed' - MSG_ERROR = _(u'Erreur') - - # on modifie la couleur de fond de la colonne résultat suivant le code_resultat - for resultat in liste_resultats: - # on met le nom du fichier en gras, après son chemin - repertoire = resultat.chemin_fichier.dirname() - if repertoire != "": repertoire += os.sep - chemin_fichier = echap(repertoire) + '' + echap(resultat.chemin_fichier.name) + '' - f.write(u'\n') - if resultat.code_resultat == Resultat.ACCEPTE: - # couleur verte - f.write(u'\n') - elif resultat.code_resultat == Resultat.NETTOYE: - # couleur jaune - f.write(u'\n') - elif resultat.code_resultat == Resultat.REFUSE \ - or resultat.code_resultat == Resultat.EXT_NON_AUTORISEE \ - or resultat.code_resultat == Resultat.FORMAT_INCORRECT \ - or resultat.code_resultat == Resultat.VIRUS : - # couleur rouge - f.write(u'\n') - elif resultat.code_resultat == Resultat.ERREUR_LECTURE \ - or resultat.code_resultat == Resultat.ERREUR_ANALYSE: - # couleur orange - f.write(u'\n') - f.write('\n\n') - - f.write('
'+_(u'Fichier analysé')+'
'+_(u'Résultat')+'
'+_(u'Commentaire')+'
' + chemin_fichier + '
'+MSG_ACCEPTED+'
'+MSG_CLEANED+'
'+MSG_BLOCKED+'
'+MSG_ERROR+'
') - if resultat.code_resultat == Resultat.EXT_NON_AUTORISEE: - f.write(echap(resultat.details()) + '
') - for raison in resultat.raison: - f.write(echap(raison) + '
') - f.write('
\n') - f.write('') - f.close() - - - # création du rapport XML "nom_rapport", ouverture en écriture - f=codecs.open(nom_rapport + ".xml", 'w', 'latin_1') - f.write(u'\n') - f.write(u'\n') - - # écriture du bandeau titre - f.write(u' \n') - f.write(u' Rapport ExeFilter v' + version + '\n') - f.write(u' ' + echap(hostname) + '\n') - f.write(u' ' + echap(username_withdomain) + '\n') - f.write(u' ' + time.strftime("%d/%m/%Y : %H:%M", time.localtime()) + '\n') - # nom répertoire avec accent dcm2205 - f.write(u' ' + echap(repertoire_src) + '\n') - if repertoire_dest: - f.write(u' ' + echap(repertoire_dest) + '\n') - f.write(u' \n') - - # affichage d'un message particulier s'il y a eu une interruption pendant l'analyse - if continuer_transfert == False: - f.write(u' ATTENTION ! \nL\'analyse a été interrompue pendant son exécution : tous les fichiers n\'ont pas été analysés.\n') - - # écriture du résumé (nb fichiers analysés, nb fichiers acceptés, etc.) - f.write(u' \n') - f.write(u' ' + str(nb_fichier) + '\n') - f.write(u' ' + str(nb_accepte) + '\n') - f.write(u' ' + str(nb_nettoye) + '\n') - f.write(u' ' + str(nb_refuse) + '\n') - f.write(u" " + str(nb_erreur) + '\n') - f.write(u' \n') - - # écriture du tableau contenant les fichiers analysés - f.write(u' \n') - - for resultat in liste_resultats: - - f.write(u' \n') - f.write(u' ' + echap(resultat.chemin_fichier) + '\n') - if resultat.code_resultat == Resultat.ACCEPTE: - f.write(u' Accepté\n') - elif resultat.code_resultat == Resultat.NETTOYE: - f.write(u' Nettoyé\n') - elif resultat.code_resultat == Resultat.REFUSE \ - or resultat.code_resultat == Resultat.EXT_NON_AUTORISEE \ - or resultat.code_resultat == Resultat.FORMAT_INCORRECT \ - or resultat.code_resultat == Resultat.VIRUS : - f.write(u' Refusé\n') - elif resultat.code_resultat == Resultat.ERREUR_LECTURE \ - or resultat.code_resultat == Resultat.ERREUR_ANALYSE: - f.write(u' Erreur\n') - if resultat.code_resultat == Resultat.EXT_NON_AUTORISEE: - f.write(u' ' +echap(resultat.details()) + '\n') - for raison in resultat.raison: - f.write(u' ' +echap(raison) + '\n') - f.write(u' \n') - - f.write(u' \n') - f.write(u'') - f.close() - -## # ouverture auto du rapport si on est en mode debug (ne marche que sous Windows) -## if mode_debug() and sys.platform == 'win32' : -## os.startfile(nom_rapport + ".html") - - # on retourne le résumé de l'analyse dans une liste - resume = (nb_fichier, nb_accepte, nb_nettoye, nb_refuse, nb_erreur) - return resume - - +#!/usr/bin/python +# -*- coding: iso-8859-1 -*- +""" +Rapport - ExeFilter + +Module pour générer des rapports aux formats HTML et XML après un transfert. + +Ce fichier fait partie du projet ExeFilter. +URL du projet: http://www.decalage.info/exefilter + +@organization: DGA/CELAR +@author: U{Philippe Lagadec} +@author: U{Arnaud Kerréneur} + +@contact: U{Philippe Lagadec} + +@copyright: DGA/CELAR 2004-2008 +@copyright: NATO/NC3A 2008-2010 (modifications PL apres ExeFilter v1.1.0) + +@license: CeCILL (open-source compatible GPL) + cf. code source ou fichier LICENCE.txt joint + +@version: 1.05 + +@status: beta +""" +__docformat__ = 'epytext en' + +__date__ = "2011-05-01" +__version__ = "1.05" + +#------------------------------------------------------------------------------ +# LICENCE pour le projet ExeFilter: + +# Copyright DGA/CELAR 2004-2008 +# Copyright NATO/NC3A 2008-2010 (PL changes after ExeFilter v1.1.0) +# Auteurs: +# - Philippe Lagadec (PL) - philippe.lagadec(a)laposte.net +# - Arnaud Kerréneur (AK) - arnaud.kerreneur(a)dga.defense.gouv.fr +# - Tanguy Vinceleux (TV) - tanguy.vinceleux(a)dga.defense.gouv.fr +# +# Ce logiciel est régi par la licence CeCILL soumise au droit français et +# respectant les principes de diffusion des logiciels libres. Vous pouvez +# utiliser, modifier et/ou redistribuer ce programme sous les conditions +# de la licence CeCILL telle que diffusée par le CEA, le CNRS et l'INRIA +# sur le site "http://www.cecill.info". Une copie de cette licence est jointe +# dans les fichiers Licence_CeCILL_V2-fr.html et Licence_CeCILL_V2-en.html. +# +# En contrepartie de l'accessibilité au code source et des droits de copie, +# de modification et de redistribution accordés par cette licence, il n'est +# offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, +# seule une responsabilité restreinte pèse sur l'auteur du programme, le +# titulaire des droits patrimoniaux et les concédants successifs. +# +# A cet égard l'attention de l'utilisateur est attirée sur les risques +# associés au chargement, à l'utilisation, à la modification et/ou au +# développement et à la reproduction du logiciel par l'utilisateur étant +# donné sa spécificité de logiciel libre, qui peut le rendre complexe à +# manipuler et qui le réserve donc à des développeurs et des professionnels +# avertis possédant des connaissances informatiques approfondies. Les +# utilisateurs sont donc invités à charger et tester l'adéquation du +# logiciel à leurs besoins dans des conditions permettant d'assurer la +# sécurité de leurs systèmes et ou de leurs données et, plus généralement, +# à l'utiliser et l'exploiter dans les mêmes conditions de sécurité. +# +# Le fait que vous puissiez accéder à cet en-tête signifie que vous avez +# pris connaissance de la licence CeCILL, et que vous en avez accepté les +# termes. + +#------------------------------------------------------------------------------ +# HISTORIQUE: +# 14/01/2005 v0.01 AK: - 1ère version +# 2005-2007 PL,AK: - nombreuses evolutions +# 12/01/2007 v1.00 PL: - version 1.00 officielle +# 2007-09-18 v1.01 PL: - ajout licence CeCILL +# - ajout get_username pour améliorer la portabilité +# - renommage de parametres generer_rapport +# - retrait date de la version ExeFilter des rapports +# 2008-03-23 v1.02 PL: - ajout _() a chaque constante chaine pour traduction +# 2010-02-07 v1.03 PL: - removed path module import +# 2010-02-09 v1.04 PL: - workaround when username cannot be determined +# 2011-05-01 v1.05 PL: - fix when destination dir is None +# - added initial support for scan mode + +#------------------------------------------------------------------------------ +# A FAIRE: + +# EVOLUTIONS ENVISAGEES: +# + generer_rapport(): utiliser politique plutôt que nom_rapport ? +# + pour l'affichage du répertoire source, afficher le path complet (avec lettre de volume) +# ? générer un rapport au format Excel (l'IHM laisserait le choix entre html et xls) ? +# - paramètres pour choisir quels types de rapport générer (d'après politique) +# ? l'ouverture automatique du rapport HTML devrait être un paramètre d'ExeFilter +# - séparer les rapports HTML et XML dans 2 fonctions +# - utiliser cElementTree pour simplifier la génération de code XML (voire XHTML?) +#------------------------------------------------------------------------------ + +#=== IMPORTS ================================================================== + +import sys, os, socket, codecs, xml.sax.saxutils, os.path, time, locale + +# modules du projet: +import commun +from commun import * +import Resultat + +#=== CONSTANTES =============================================================== + +#=== VARIABLES GLOBALES ======================================================= + +# liste des objets Resultats qui vont servir à générer le rapport final: +# (vide au départ) +#TODO: deplacer cette liste dans l'objet ExeFilter +liste_resultats = [] + +#------------------------------------------------------------------------------ +# AJOUTER_RESULTAT +#------------------- + +def ajouter_resultat (resultat): + """Pour ajouter un résultat à la liste des résultats, qui sert ensuite à + produire un rapport global.""" + # liste_resultats est une variable globale, il faut le préciser: + global liste_resultats + liste_resultats.append(resultat) + +#------------------------------------------------------------------------------ +# ECHAP +#------------------- + +def echap (chaine): + """ + Pour remplacer tous les caractères ayant une signification en HTML + comme '<', '>' ou '&' par un encodage correct: '<', '>', '&', ... + """ + if chaine: + return xml.sax.saxutils.escape(chaine) + else: + return '' + +#------------------------------------------------------------------------------ +# GENERER_RAPPORT +#------------------- + +def generer_rapport(nom_rapport, repertoire_src, repertoire_dest, version, + date_version, continuer_transfert): + """ + Génère un rapport au format HTML et XML. + + Retourne le résumé de l'analyse dans une liste. + + @param repertoire_src : chemin du répertoire analysé + @param nom_rapport : nom des fichiers rapports HTML/XML (sans extension) + @param version : numéro de version du logiciel + @param date_version : date de version du logiciel + @param continuer_transfert: booléen pour savoir s'il y a eu interruption de l'analyse + """ + # initialisation des variables compteurs (nb fichiers analysés, + # nb fichiers acceptés, etc.) + nb_fichier = 0 + nb_accepte = 0 + nb_nettoye = 0 + nb_refuse = 0 + nb_erreur = 0 + + # création du répertoire des rapports s'ils n'existe pas: + #p = path(politique.parametres['rep_rapports'].valeur) + p = os.path.dirname(nom_rapport) + if not os.path.exists(p): + #p.makedirs() + os.makedirs(p) + + # on incrémente les variables compteurs + for resultat in liste_resultats: + nb_fichier = nb_fichier+1 + if resultat.code_resultat == Resultat.ACCEPTE: + nb_accepte = nb_accepte+1 + elif resultat.code_resultat == Resultat.NETTOYE: + nb_nettoye = nb_nettoye+1 + elif resultat.code_resultat == Resultat.REFUSE \ + or resultat.code_resultat == Resultat.EXT_NON_AUTORISEE \ + or resultat.code_resultat == Resultat.FORMAT_INCORRECT \ + or resultat.code_resultat == Resultat.VIRUS : + nb_refuse = nb_refuse+1 + elif resultat.code_resultat == Resultat.ERREUR_LECTURE \ + or resultat.code_resultat == Resultat.ERREUR_ANALYSE: + nb_erreur = nb_erreur+1 + + # on récupère le nom de l'utilisateur qui lance ExeFilter, avec nom de + # domaine (ou de machine) sous Windows: + try: + username_withdomain = get_username(with_domain=True) + except: + # workaround if user name cannot be determined + username_withdomain = 'unknown' + hostname = socket.gethostname() + + # création du rapport HTML "nom_rapport", ouverture en écriture + f=codecs.open(nom_rapport + ".html", 'w', 'latin_1') + # le rapport est un fichier encodé en Latin-1, pour éviter les erreurs unicode: + #f = codecs.EncodedFile (f1, 'latin_1', 'latin_1') + f.write(u'\n') + f.write(u'\n') + f.write(u'\n') + f.write(_(u'Rapport ExeFilter\n')) + f.write(u'\n') + f.write(u'\n\n') + f.write(u'\n') + + # écriture du bandeau titre + f.write(u'
\n') + f.write(_(u'Rapport ExeFilter v%(version)s') % {'version': version} + '\n
') + date = time.strftime("%c", time.localtime()) + f.write(_(u"généré le %(date)s sur la machine %(hostname)s par l'utilisateur %(user)s")\ + % {'date':date, 'hostname':hostname, 'user':username_withdomain}) + f.write("\n
") + if repertoire_dest: + f.write(_(u'Répertoire de destination : ') + echap(repertoire_dest)) + f.write('\n
') + f.write(_(u'Répertoire(s) et/ou fichier(s) analysé(s) : ') + echap(repertoire_src)) + f.write(u'\n

\n\n') + + # affichage d'un message particulier s'il y a eu une interruption pendant l'analyse + if continuer_transfert == False: + f.write(u'
') + f.write(_(u"ATTENTION ! L'analyse a été interrompue pendant son exécution : ")) + f.write(_(u"tous les fichiers n'ont pas été analysés.")) + f.write(u'
') + + # écriture du résumé (nb fichiers analysés, nb fichiers acceptés, etc.) + f.write('
'+_(u'RESUME :')+'
\n') + f.write(_(u'Nombre de fichiers analysés : ') + str(nb_fichier) + '
\n') + if commun.clean_mode: + f.write(_(u'Nombre de fichiers acceptés : ') + str(nb_accepte) + '
\n') + f.write(_(u'Nombre de fichiers nettoyés : ') + str(nb_nettoye) + '
\n') + f.write(_(u'Nombre de fichiers refusés : ') + str(nb_refuse) + '
\n') + else: + f.write(u'Number of clean files : ' + str(nb_accepte) + '
\n') + f.write(u'Number of files to be cleaned: ' + str(nb_nettoye) + '
\n') + f.write(u'Number of not allowed files : ' + str(nb_refuse) + '
\n') + f.write(_(u"Nombre d'erreurs : ") + str(nb_erreur) + '
\n\n') + + # écriture du tableau contenant les fichiers analysés + f.write(u'
\n') + f.write(u'\n') + f.write(u'\n') + f.write(u'\n') + f.write(u'\n\n') + + if commun.clean_mode: + MSG_ACCEPTED = _(u'Accepté') + MSG_CLEANED = _(u'Nettoyé') + MSG_BLOCKED = _(u'Refusé') + MSG_ERROR = _(u'Erreur') + else: + MSG_ACCEPTED = u'Clean' + MSG_CLEANED = u'To be cleaned' + MSG_BLOCKED = u'Not allowed' + MSG_ERROR = _(u'Erreur') + + # on modifie la couleur de fond de la colonne résultat suivant le code_resultat + for resultat in liste_resultats: + # on met le nom du fichier en gras, après son chemin + repertoire = resultat.chemin_fichier.dirname() + if repertoire != "": repertoire += os.sep + chemin_fichier = echap(repertoire) + '' + echap(resultat.chemin_fichier.name) + '' + f.write(u'\n') + if resultat.code_resultat == Resultat.ACCEPTE: + # couleur verte + f.write(u'\n') + elif resultat.code_resultat == Resultat.NETTOYE: + # couleur jaune + f.write(u'\n') + elif resultat.code_resultat == Resultat.REFUSE \ + or resultat.code_resultat == Resultat.EXT_NON_AUTORISEE \ + or resultat.code_resultat == Resultat.FORMAT_INCORRECT \ + or resultat.code_resultat == Resultat.VIRUS : + # couleur rouge + f.write(u'\n') + elif resultat.code_resultat == Resultat.ERREUR_LECTURE \ + or resultat.code_resultat == Resultat.ERREUR_ANALYSE: + # couleur orange + f.write(u'\n') + f.write('\n\n') + + f.write('
'+_(u'Fichier analysé')+'
'+_(u'Résultat')+'
'+_(u'Commentaire')+'
' + chemin_fichier + '
'+MSG_ACCEPTED+'
'+MSG_CLEANED+'
'+MSG_BLOCKED+'
'+MSG_ERROR+'
') + if resultat.code_resultat == Resultat.EXT_NON_AUTORISEE: + f.write(echap(resultat.details()) + '
') + for raison in resultat.raison: + f.write(echap(raison) + '
') + f.write('
\n') + f.write('') + f.close() + + + # création du rapport XML "nom_rapport", ouverture en écriture + f=codecs.open(nom_rapport + ".xml", 'w', 'latin_1') + f.write(u'\n') + f.write(u'\n') + + # écriture du bandeau titre + f.write(u' \n') + f.write(u' Rapport ExeFilter v' + version + '\n') + f.write(u' ' + echap(hostname) + '\n') + f.write(u' ' + echap(username_withdomain) + '\n') + f.write(u' ' + time.strftime("%d/%m/%Y : %H:%M", time.localtime()) + '\n') + # nom répertoire avec accent dcm2205 + f.write(u' ' + echap(repertoire_src) + '\n') + if repertoire_dest: + f.write(u' ' + echap(repertoire_dest) + '\n') + f.write(u' \n') + + # affichage d'un message particulier s'il y a eu une interruption pendant l'analyse + if continuer_transfert == False: + f.write(u' ATTENTION ! \nL\'analyse a été interrompue pendant son exécution : tous les fichiers n\'ont pas été analysés.\n') + + # écriture du résumé (nb fichiers analysés, nb fichiers acceptés, etc.) + f.write(u' \n') + f.write(u' ' + str(nb_fichier) + '\n') + f.write(u' ' + str(nb_accepte) + '\n') + f.write(u' ' + str(nb_nettoye) + '\n') + f.write(u' ' + str(nb_refuse) + '\n') + f.write(u" " + str(nb_erreur) + '\n') + f.write(u' \n') + + # écriture du tableau contenant les fichiers analysés + f.write(u' \n') + + for resultat in liste_resultats: + + f.write(u' \n') + f.write(u' ' + echap(resultat.chemin_fichier) + '\n') + if resultat.code_resultat == Resultat.ACCEPTE: + f.write(u' Accepté\n') + elif resultat.code_resultat == Resultat.NETTOYE: + f.write(u' Nettoyé\n') + elif resultat.code_resultat == Resultat.REFUSE \ + or resultat.code_resultat == Resultat.EXT_NON_AUTORISEE \ + or resultat.code_resultat == Resultat.FORMAT_INCORRECT \ + or resultat.code_resultat == Resultat.VIRUS : + f.write(u' Refusé\n') + elif resultat.code_resultat == Resultat.ERREUR_LECTURE \ + or resultat.code_resultat == Resultat.ERREUR_ANALYSE: + f.write(u' Erreur\n') + if resultat.code_resultat == Resultat.EXT_NON_AUTORISEE: + f.write(u' ' +echap(resultat.details()) + '\n') + for raison in resultat.raison: + f.write(u' ' +echap(raison) + '\n') + f.write(u' \n') + + f.write(u' \n') + f.write(u'') + f.close() + +## # ouverture auto du rapport si on est en mode debug (ne marche que sous Windows) +## if mode_debug() and sys.platform == 'win32' : +## os.startfile(nom_rapport + ".html") + + # on retourne le résumé de l'analyse dans une liste + resume = (nb_fichier, nb_accepte, nb_nettoye, nb_refuse, nb_erreur) + return resume + + diff --git a/Resultat.py b/Resultat.py index 570973c..10e60e9 100644 --- a/Resultat.py +++ b/Resultat.py @@ -1,259 +1,259 @@ -#!/usr/bin/python -# -*- coding: iso-8859-1 -*- -""" -Resultat - ExeFilter - -Module qui contient la classe L{Resultat.Resultat}, -afin de décrire le résultat des filtres appliqués à un fichier. - -Ce fichier fait partie du projet ExeFilter. -URL du projet: http://www.decalage.info/exefilter - -@organization: DGA/CELAR, NATO/NC3A -@author: U{Philippe Lagadec} -@author: U{Arnaud Kerréneur} - -@contact: U{Philippe Lagadec} - -@copyright: DGA/CELAR 2004-2008 -@copyright: NATO/NC3A 2008 (modifications PL apres v1.1.0) - -@license: CeCILL (open-source compatible GPL) - cf. code source ou fichier LICENCE.txt joint - -@version: 1.03 - -@status: beta -""" -#============================================================================== -__docformat__ = 'epytext en' - -__date__ = "2011-04-17" -__version__ = "1.03" - -#------------------------------------------------------------------------------ -# LICENCE pour le projet ExeFilter: - -# Copyright DGA/CELAR 2004-2008 -# Copyright NATO/NC3A 2008 (PL changes after v1.1.0) -# Auteurs: -# - Philippe Lagadec (PL) - philippe.lagadec(a)laposte.net -# - Arnaud Kerréneur (AK) - arnaud.kerreneur(a)dga.defense.gouv.fr -# - Tanguy Vinceleux (TV) - tanguy.vinceleux(a)dga.defense.gouv.fr -# -# Ce logiciel est régi par la licence CeCILL soumise au droit français et -# respectant les principes de diffusion des logiciels libres. Vous pouvez -# utiliser, modifier et/ou redistribuer ce programme sous les conditions -# de la licence CeCILL telle que diffusée par le CEA, le CNRS et l'INRIA -# sur le site "http://www.cecill.info". Une copie de cette licence est jointe -# dans les fichiers Licence_CeCILL_V2-fr.html et Licence_CeCILL_V2-en.html. -# -# En contrepartie de l'accessibilité au code source et des droits de copie, -# de modification et de redistribution accordés par cette licence, il n'est -# offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, -# seule une responsabilité restreinte pèse sur l'auteur du programme, le -# titulaire des droits patrimoniaux et les concédants successifs. -# -# A cet égard l'attention de l'utilisateur est attirée sur les risques -# associés au chargement, à l'utilisation, à la modification et/ou au -# développement et à la reproduction du logiciel par l'utilisateur étant -# donné sa spécificité de logiciel libre, qui peut le rendre complexe à -# manipuler et qui le réserve donc à des développeurs et des professionnels -# avertis possédant des connaissances informatiques approfondies. Les -# utilisateurs sont donc invités à charger et tester l'adéquation du -# logiciel à leurs besoins dans des conditions permettant d'assurer la -# sécurité de leurs systèmes et ou de leurs données et, plus généralement, -# à l'utiliser et l'exploiter dans les mêmes conditions de sécurité. -# -# Le fait que vous puissiez accéder à cet en-tête signifie que vous avez -# pris connaissance de la licence CeCILL, et que vous en avez accepté les -# termes. - -#------------------------------------------------------------------------------ -# HISTORIQUE: -# 14/01/2005 v0.01 AK: - 1ère version -# 2005-2007 PL,AK: - nombreuses evolutions -# 12/01/2007 v1.00 PL: - version 1.00 officielle -# 2007-09-18 v1.01 PL: - ajout licence CeCILL -# 2008-03-24 v1.02 PL: - ajout de _() pour traduction gettext des chaines -# 2011-04-17 v1.03 PL: - added detailed result strings for scan-only mode - -# A FAIRE: -#------------------------------------------------------------------------------ - -#=== IMPORTS ================================================================== - -import sys, copy - -# modules du projet: -import commun -from commun import * -import Fichier - -#=== CONSTANTES =============================================================== - -# Codes des Résultats possibles pour l'analyse d'un fichier, par ordre de -# priorité croissante: -#NON_RECONNU = 0 -EXT_NON_AUTORISEE = 0 -FORMAT_INCORRECT = 10 -ACCEPTE = 20 -NETTOYE = 30 -ERREUR_LECTURE = 40 -ERREUR_ANALYSE = 50 -REFUSE = 60 -VIRUS = 70 - -# résultat détaillé sous forme de chaîne pour chaque code résultat: -resultat_detaille = { - EXT_NON_AUTORISEE : _(u"Extension non autorisée"), - FORMAT_INCORRECT : _(u"Format incorrect ou incompatible avec l'extension"), - ACCEPTE : _(u"Fichier accepté"), - NETTOYE : _(u"Fichier nettoyé"), - ERREUR_LECTURE : _(u"Erreur de lecture"), - ERREUR_ANALYSE : _(u"Erreur lors de l'analyse"), - REFUSE : _(u"Fichier refusé (analyse ou nettoyage impossible)"), - VIRUS : _(u"Fichier infecté par un virus") - } -# Detailed result strings for scan-only mode -resultat_detaille_scan = { - EXT_NON_AUTORISEE : 'File extension not allowed', - FORMAT_INCORRECT : 'File format not allowed or not matching extension', - ACCEPTE : 'Allowed and clean', - NETTOYE : 'Contains active content that can be cleaned', - ERREUR_LECTURE : 'Read error', - ERREUR_ANALYSE : 'Analysis error', - REFUSE : 'Cannot be analyzed or cleaned', - VIRUS : 'Infected by a virus' - } - -# indique pour chaque code résultat s'il s'agit d'un refus du fichier: -resultat_refuse = { - EXT_NON_AUTORISEE : True, - FORMAT_INCORRECT : True, - ACCEPTE : False, - NETTOYE : False, - ERREUR_LECTURE : True, - ERREUR_ANALYSE : True, - REFUSE : True, - VIRUS : True - } - -#------------------------------------------------------------------------------ -# Classe Resultat -#------------------- -class Resultat: - """classe pour afficher le résultat d'une analyse""" - - # NOTE: un objet Resultat NE DOIT PAS contenir une référence à - # un objet Fichier, car on doit pouvoir conserver une liste - # globale de résultats pour un transfert, sans que les objets - # Fichier et Conteneur restent en mémoire. Dans le constructeur - # ci-dessous, le paramètre fichier sert juste à récupérer le - # chemin complet. - - def __init__(self, code_resultat=EXT_NON_AUTORISEE, raison="", - fichier=None): - """ - Constructeur de la classe Resultat. - - @param code_resultat: code (numéro) associé au résultat, cf. constantes du module L{Resultat} - @type code_resultat: int - - @param raison: explique le résultat de l'analyse - @type raison: str, unicode - - @param fichier: fichier analysé - @type fichier: L{Fichier.Fichier} - """ - self.code_resultat = code_resultat - # raison doit être une liste de chaînes unicode. - self.raison = [] - self.ajouter_raison(raison) - assert isinstance(fichier, Fichier.Fichier) - self.chemin_fichier = fichier.chemin_complet - - def ajouter_raison (self, raison): - """Pour ajouter une raison ou une liste de raisons, en convertissant - toutes les chaînes str en unicode.""" - # si raison est une chaîne, on la transforme en liste. - # et si c'est une chaîne vide, la liste doit être vide: - if isinstance(raison, str) or isinstance(raison, unicode): - if raison == "": - raison = [] - else: - raison = [raison] - # ensuite on parcourt toutes les chaînes de la liste: - # (on suppose que c'est une liste, il faudrait peut-être le vérifier) - for chaine in raison: - # si c'est une chaîne str, on la tranforme de latin_1 en unicode: - if isinstance(chaine, str): - self.raison.append( unicode(chaine, 'latin_1') ) - else: - self.raison.append(chaine) - - def ajouter (self, resultat): - """Fusionne avec un autre résultat (en gardant le plus - prioritaire), et ajoute la raison à la liste déjà présente.""" - # si le code_resultat est le même, on ajoute la raison à la - # liste des raisons déjà présentes: - if resultat.code_resultat == self.code_resultat: - self.ajouter_raison(resultat.raison) - # sinon on prend le nouveau code_resultat (si > à l'ancien) - # et on ajoute la raison à la liste déjà présente - elif resultat.code_resultat > self.code_resultat: - # si le code_resultat précédent est FORMAT_INCORRECT, - # alors on supprime la raison précédente, car on vient - # de trouver un format qui convient: - if self.code_resultat == FORMAT_INCORRECT: - self.raison = [] - self.code_resultat = resultat.code_resultat - self.ajouter_raison(resultat.raison) - # on ne met à jour chemin_fichier que s'il était vide: - if self.chemin_fichier == "": - self.chemin_fichier = resultat.chemin_fichier - - def ajouter_conteneur (self, liste_resultats, type_conteneur="Conteneur"): - """Ajoute la liste des résultats des fichiers inclus dans un - conteneur à resultats_conteneur, puis fusionne l'ensemble des - résultats individuels pour obtenir le résultat global. - - @param liste_resultats: liste d'objets Resultat de chaque fichier du conteneur - @param type_conteneur: chaîne nommant le type de conteneur - @type type_conteneur: str, unicode - """ - # on fusionne l'ensemble des résultats: - # - accepté si tous les fichiers sont acceptés - # - nettoyé si au moins 1 accepté et 1 refusé - # - refusé si tous sont refusés - nb_acceptes = 0 - nb_refuses = 0 - nb_nettoyes = 0 - for resultat in liste_resultats: - if resultat.est_refuse(): - nb_refuses += 1 - else: - nb_acceptes += 1 - if resultat.code_resultat == NETTOYE: - nb_nettoyes += 1 - if (nb_acceptes>0 and nb_refuses>0) or nb_nettoyes>0: - self.code_resultat = NETTOYE - elif nb_acceptes>0 and nb_refuses==0 and nb_nettoyes==0: - self.code_resultat = ACCEPTE - else: - self.code_resultat = REFUSE - # pour terminer, on ne garde qu'une raison globale, déduite du résultat obtenu: - self.raison = [ type_conteneur + ' : ' + self.details() ] - - def details (self): - "Retourne une chaîne détaillant le code résultat." - if commun.clean_mode: - return resultat_detaille[ self.code_resultat ] - else: - return resultat_detaille_scan[ self.code_resultat ] - - def est_refuse (self): - "Retourne True si le résultat correspond à un refus." - return resultat_refuse[ self.code_resultat ] - - +#!/usr/bin/python +# -*- coding: iso-8859-1 -*- +""" +Resultat - ExeFilter + +Module qui contient la classe L{Resultat.Resultat}, +afin de décrire le résultat des filtres appliqués à un fichier. + +Ce fichier fait partie du projet ExeFilter. +URL du projet: http://www.decalage.info/exefilter + +@organization: DGA/CELAR, NATO/NC3A +@author: U{Philippe Lagadec} +@author: U{Arnaud Kerréneur} + +@contact: U{Philippe Lagadec} + +@copyright: DGA/CELAR 2004-2008 +@copyright: NATO/NC3A 2008 (modifications PL apres v1.1.0) + +@license: CeCILL (open-source compatible GPL) + cf. code source ou fichier LICENCE.txt joint + +@version: 1.03 + +@status: beta +""" +#============================================================================== +__docformat__ = 'epytext en' + +__date__ = "2011-04-17" +__version__ = "1.03" + +#------------------------------------------------------------------------------ +# LICENCE pour le projet ExeFilter: + +# Copyright DGA/CELAR 2004-2008 +# Copyright NATO/NC3A 2008 (PL changes after v1.1.0) +# Auteurs: +# - Philippe Lagadec (PL) - philippe.lagadec(a)laposte.net +# - Arnaud Kerréneur (AK) - arnaud.kerreneur(a)dga.defense.gouv.fr +# - Tanguy Vinceleux (TV) - tanguy.vinceleux(a)dga.defense.gouv.fr +# +# Ce logiciel est régi par la licence CeCILL soumise au droit français et +# respectant les principes de diffusion des logiciels libres. Vous pouvez +# utiliser, modifier et/ou redistribuer ce programme sous les conditions +# de la licence CeCILL telle que diffusée par le CEA, le CNRS et l'INRIA +# sur le site "http://www.cecill.info". Une copie de cette licence est jointe +# dans les fichiers Licence_CeCILL_V2-fr.html et Licence_CeCILL_V2-en.html. +# +# En contrepartie de l'accessibilité au code source et des droits de copie, +# de modification et de redistribution accordés par cette licence, il n'est +# offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, +# seule une responsabilité restreinte pèse sur l'auteur du programme, le +# titulaire des droits patrimoniaux et les concédants successifs. +# +# A cet égard l'attention de l'utilisateur est attirée sur les risques +# associés au chargement, à l'utilisation, à la modification et/ou au +# développement et à la reproduction du logiciel par l'utilisateur étant +# donné sa spécificité de logiciel libre, qui peut le rendre complexe à +# manipuler et qui le réserve donc à des développeurs et des professionnels +# avertis possédant des connaissances informatiques approfondies. Les +# utilisateurs sont donc invités à charger et tester l'adéquation du +# logiciel à leurs besoins dans des conditions permettant d'assurer la +# sécurité de leurs systèmes et ou de leurs données et, plus généralement, +# à l'utiliser et l'exploiter dans les mêmes conditions de sécurité. +# +# Le fait que vous puissiez accéder à cet en-tête signifie que vous avez +# pris connaissance de la licence CeCILL, et que vous en avez accepté les +# termes. + +#------------------------------------------------------------------------------ +# HISTORIQUE: +# 14/01/2005 v0.01 AK: - 1ère version +# 2005-2007 PL,AK: - nombreuses evolutions +# 12/01/2007 v1.00 PL: - version 1.00 officielle +# 2007-09-18 v1.01 PL: - ajout licence CeCILL +# 2008-03-24 v1.02 PL: - ajout de _() pour traduction gettext des chaines +# 2011-04-17 v1.03 PL: - added detailed result strings for scan-only mode + +# A FAIRE: +#------------------------------------------------------------------------------ + +#=== IMPORTS ================================================================== + +import sys, copy + +# modules du projet: +import commun +from commun import * +import Fichier + +#=== CONSTANTES =============================================================== + +# Codes des Résultats possibles pour l'analyse d'un fichier, par ordre de +# priorité croissante: +#NON_RECONNU = 0 +EXT_NON_AUTORISEE = 0 +FORMAT_INCORRECT = 10 +ACCEPTE = 20 +NETTOYE = 30 +ERREUR_LECTURE = 40 +ERREUR_ANALYSE = 50 +REFUSE = 60 +VIRUS = 70 + +# résultat détaillé sous forme de chaîne pour chaque code résultat: +resultat_detaille = { + EXT_NON_AUTORISEE : _(u"Extension non autorisée"), + FORMAT_INCORRECT : _(u"Format incorrect ou incompatible avec l'extension"), + ACCEPTE : _(u"Fichier accepté"), + NETTOYE : _(u"Fichier nettoyé"), + ERREUR_LECTURE : _(u"Erreur de lecture"), + ERREUR_ANALYSE : _(u"Erreur lors de l'analyse"), + REFUSE : _(u"Fichier refusé (analyse ou nettoyage impossible)"), + VIRUS : _(u"Fichier infecté par un virus") + } +# Detailed result strings for scan-only mode +resultat_detaille_scan = { + EXT_NON_AUTORISEE : 'File extension not allowed', + FORMAT_INCORRECT : 'File format not allowed or not matching extension', + ACCEPTE : 'Allowed and clean', + NETTOYE : 'Contains active content that can be cleaned', + ERREUR_LECTURE : 'Read error', + ERREUR_ANALYSE : 'Analysis error', + REFUSE : 'Cannot be analyzed or cleaned', + VIRUS : 'Infected by a virus' + } + +# indique pour chaque code résultat s'il s'agit d'un refus du fichier: +resultat_refuse = { + EXT_NON_AUTORISEE : True, + FORMAT_INCORRECT : True, + ACCEPTE : False, + NETTOYE : False, + ERREUR_LECTURE : True, + ERREUR_ANALYSE : True, + REFUSE : True, + VIRUS : True + } + +#------------------------------------------------------------------------------ +# Classe Resultat +#------------------- +class Resultat: + """classe pour afficher le résultat d'une analyse""" + + # NOTE: un objet Resultat NE DOIT PAS contenir une référence à + # un objet Fichier, car on doit pouvoir conserver une liste + # globale de résultats pour un transfert, sans que les objets + # Fichier et Conteneur restent en mémoire. Dans le constructeur + # ci-dessous, le paramètre fichier sert juste à récupérer le + # chemin complet. + + def __init__(self, code_resultat=EXT_NON_AUTORISEE, raison="", + fichier=None): + """ + Constructeur de la classe Resultat. + + @param code_resultat: code (numéro) associé au résultat, cf. constantes du module L{Resultat} + @type code_resultat: int + + @param raison: explique le résultat de l'analyse + @type raison: str, unicode + + @param fichier: fichier analysé + @type fichier: L{Fichier.Fichier} + """ + self.code_resultat = code_resultat + # raison doit être une liste de chaînes unicode. + self.raison = [] + self.ajouter_raison(raison) + assert isinstance(fichier, Fichier.Fichier) + self.chemin_fichier = fichier.chemin_complet + + def ajouter_raison (self, raison): + """Pour ajouter une raison ou une liste de raisons, en convertissant + toutes les chaînes str en unicode.""" + # si raison est une chaîne, on la transforme en liste. + # et si c'est une chaîne vide, la liste doit être vide: + if isinstance(raison, (str, bytes)): + if raison == "" or raison == b"": + raison = [] + else: + raison = [raison] + # ensuite on parcourt toutes les chaînes de la liste: + # (on suppose que c'est une liste, il faudrait peut-être le vérifier) + for chaine in raison: + # si c'est une chaîne str, on la tranforme de latin_1 en unicode: + if isinstance(chaine, bytes): + self.raison.append( chaine.decode('latin_1') ) + else: + self.raison.append(chaine) + + def ajouter (self, resultat): + """Fusionne avec un autre résultat (en gardant le plus + prioritaire), et ajoute la raison à la liste déjà présente.""" + # si le code_resultat est le même, on ajoute la raison à la + # liste des raisons déjà présentes: + if resultat.code_resultat == self.code_resultat: + self.ajouter_raison(resultat.raison) + # sinon on prend le nouveau code_resultat (si > à l'ancien) + # et on ajoute la raison à la liste déjà présente + elif resultat.code_resultat > self.code_resultat: + # si le code_resultat précédent est FORMAT_INCORRECT, + # alors on supprime la raison précédente, car on vient + # de trouver un format qui convient: + if self.code_resultat == FORMAT_INCORRECT: + self.raison = [] + self.code_resultat = resultat.code_resultat + self.ajouter_raison(resultat.raison) + # on ne met à jour chemin_fichier que s'il était vide: + if self.chemin_fichier == "": + self.chemin_fichier = resultat.chemin_fichier + + def ajouter_conteneur (self, liste_resultats, type_conteneur="Conteneur"): + """Ajoute la liste des résultats des fichiers inclus dans un + conteneur à resultats_conteneur, puis fusionne l'ensemble des + résultats individuels pour obtenir le résultat global. + + @param liste_resultats: liste d'objets Resultat de chaque fichier du conteneur + @param type_conteneur: chaîne nommant le type de conteneur + @type type_conteneur: str, unicode + """ + # on fusionne l'ensemble des résultats: + # - accepté si tous les fichiers sont acceptés + # - nettoyé si au moins 1 accepté et 1 refusé + # - refusé si tous sont refusés + nb_acceptes = 0 + nb_refuses = 0 + nb_nettoyes = 0 + for resultat in liste_resultats: + if resultat.est_refuse(): + nb_refuses += 1 + else: + nb_acceptes += 1 + if resultat.code_resultat == NETTOYE: + nb_nettoyes += 1 + if (nb_acceptes>0 and nb_refuses>0) or nb_nettoyes>0: + self.code_resultat = NETTOYE + elif nb_acceptes>0 and nb_refuses==0 and nb_nettoyes==0: + self.code_resultat = ACCEPTE + else: + self.code_resultat = REFUSE + # pour terminer, on ne garde qu'une raison globale, déduite du résultat obtenu: + self.raison = [ type_conteneur + ' : ' + self.details() ] + + def details (self): + "Retourne une chaîne détaillant le code résultat." + if commun.clean_mode: + return resultat_detaille[ self.code_resultat ] + else: + return resultat_detaille_scan[ self.code_resultat ] + + def est_refuse (self): + "Retourne True si le résultat correspond à un refus." + return resultat_refuse[ self.code_resultat ] + + diff --git a/commun.py b/commun.py index 1e0aa5e..16185ef 100644 --- a/commun.py +++ b/commun.py @@ -1,328 +1,328 @@ -#!/usr/bin/python -# -*- coding: iso-8859-1 -*- -""" -ExeFilter - Fonctions, constantes et variables communes à tous les modules. - -Ce fichier fait partie du projet ExeFilter. -URL du projet: U{http://www.decalage.info/exefilter} - -@author: U{Philippe Lagadec} -@author: U{Arnaud Kerréneur} -@organization: DGA/CELAR - -@contact: U{Philippe Lagadec} - -@copyright: DGA/CELAR 2004-2008 -@copyright: NATO/NC3A 2008-2010 (modifications PL apres ExeFilter v1.1.0) - -@license: CeCILL (open-source compatible GPL) - cf. code source ou fichier LICENCE.txt joint - -@version: 1.06 -@status: beta - -@var MODE_DEBUG: Activation du mode debug -""" -__docformat__ = 'epytext en' - -#__author__ = "Philippe Lagadec, Arnaud Kerréneur (DGA/CELAR)" -__date__ = "2011-04-17" -__version__ = "1.06" - - -#------------------------------------------------------------------------------ -# LICENCE: - -# Copyright DGA/CELAR 2004-2008 -# Copyright NATO/NC3A 2008-2010 (modifications PL apres ExeFilter v1.1.0) -# -# Auteurs: -# - Philippe Lagadec (PL) - philippe.lagadec(a)laposte.net -# - Arnaud Kerréneur (AK) - arnaud.kerreneur(a)dga.defense.gouv.fr -# - Tanguy Vinceleux (TV) - tanguy.vinceleux(a)dga.defense.gouv.fr -# -# Ce logiciel est régi par la licence CeCILL soumise au droit français et -# respectant les principes de diffusion des logiciels libres. Vous pouvez -# utiliser, modifier et/ou redistribuer ce programme sous les conditions -# de la licence CeCILL telle que diffusée par le CEA, le CNRS et l'INRIA -# sur le site "http://www.cecill.info". Une copie de cette licence est jointe -# dans les fichiers Licence_CeCILL_V2-fr.html et Licence_CeCILL_V2-en.html. -# -# En contrepartie de l'accessibilité au code source et des droits de copie, -# de modification et de redistribution accordés par cette licence, il n'est -# offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, -# seule une responsabilité restreinte pèse sur l'auteur du programme, le -# titulaire des droits patrimoniaux et les concédants successifs. -# -# A cet égard l'attention de l'utilisateur est attirée sur les risques -# associés au chargement, à l'utilisation, à la modification et/ou au -# développement et à la reproduction du logiciel par l'utilisateur étant -# donné sa spécificité de logiciel libre, qui peut le rendre complexe à -# manipuler et qui le réserve donc à des développeurs et des professionnels -# avertis possédant des connaissances informatiques approfondies. Les -# utilisateurs sont donc invités à charger et tester l'adéquation du -# logiciel à leurs besoins dans des conditions permettant d'assurer la -# sécurité de leurs systèmes et ou de leurs données et, plus généralement, -# à l'utiliser et l'exploiter dans les mêmes conditions de sécurité. -# -# Le fait que vous puissiez accéder à cet en-tête signifie que vous avez -# pris connaissance de la licence CeCILL, et que vous en avez accepté les -# termes. - - -#------------------------------------------------------------------------------ -# HISTORIQUE: -# 2004-10-24 v0.01 PL: - 1ère version -# 2004-2006 PL,AK: - nombreuses evolutions -# 2007-01-12 v1.00 PL: - version 1.00 officielle -# 2007-07-24 PL: - import plx pour ameliorer la portabilite -# 2007-09-10 v1.01 PL: - ajout licence CeCILL -# - conversion tabs en espaces -# - amelioration imports -# - ajout de display_html_file -# 2010-02-04 v1.02 PL: - disabled sous_rep_temp to avoid race conditions -# 2010-02-07 v1.03 PL: - updated path module import -# 2010-02-23 v1.04 PL: - updated plx import -# 2011-02-18 v1.05 PL: - added getTempBase, newTempFile and newTempDir -# 2011-04-17 v1.06 PL: - added global for clean mode - -#------------------------------------------------------------------------------ -# TODO: -# + replace global variables by threading.local variables to support -# multithreading? -# - ajouter parametres pour le comportement de chemin_relatif_incorrect() -# - chemin_relatif_incorrect(): ajout verif variable environnement Windows %xy% -# ? chemin_relatif_incorrect(): ajout verif chemin relatif MacOS avec ':' ? - -#------------------------------------------------------------------------------ - -#=== IMPORTS ================================================================== - -# import du module path.py pour simplifier la gestion des fichiers/repertoires: -try: - from thirdparty.path.path import path -except: - raise ImportError, "the path module is not installed: "\ - "see http://pypi.python.org/pypi/path.py" - -# module plx pour ameliorer la portabilite -try: - import thirdparty.plx.plx as plx -except: - raise ImportError, "the plx module is not installed: "\ - +"see http://www.decalage.info/python/plx/" - -import tempfile, os -import thirdparty.tempfilemgr.tempfilemgr as tempfilemgr - -# modules du projet -import Journal - -#=== CONSTANTES =============================================================== - -MODE_DEBUG = False # contrôle si les messages de debug() s'affichent - -#TODO: a remplacer par attributs de la classe ExeFilter: -nb_fichiers = compteur_avancement = 0 -continuer_transfert = True -transfert_commence = False -##sous_rep_temp = None -sous_rep_archive = None -politique = None -clean_mode = True # indicates if scan-only or clean mode - - -#=== FONCTIONS ================================================================ - -# fonctions et constantes importees du module plx: -get_username = plx.get_username -display_html_file = plx.display_html_file -main_is_frozen = plx.main_is_frozen -get_main_dir = plx.get_main_dir -print_console = plx.print_console -print_oem = plx.print_console -str_console = plx.str_console -str_oem = plx.str_console -str_lat1 = plx.str_lat1 -unistr = plx.unistr -Popen_timer = plx.Popen_timer -EXIT_KILL_PTIMER = plx.EXIT_KILL_PTIMER - - -#------------------------------------------------------------------------------ -# mode_debug -#------------------- - -def mode_debug(mode=None): - """ - Pour activer ou désactiver le mode debug, en modifiant la variable - globale MODE_DEBUG. Retourne la valeur de cette variable. - """ - global MODE_DEBUG - if mode!=None: - MODE_DEBUG = bool(mode) - return MODE_DEBUG - - -#------------------------------------------------------------------------------ -# DEBUG -#------------------- -def debug(texte): - """ - pour afficher un texte de débogage si MODE_DEBUG = True - - @param texte: le texte à afficher - @type texte: str - - """ - if MODE_DEBUG: - Journal.debug(texte) - #print_oem ("DEBUG: " + texte) - - -#------------------------------------------------------------------------------ -# DEBUG_PAUSE -#------------------- -def debug_pause(texte=None): - """ - pour faire une pause si MODE_DEBUG = True, et laisser l'utilisateur - lire les messages à l'écran. - """ - if MODE_DEBUG: - if texte: debug(texte) - #debug("Appuyer sur Entree pour continuer...") - print "Appuyer sur Entree pour continuer, ou Ctrl+C pour stopper..." - attente = raw_input() - - -#------------------------------------------------------------------------------ -# EFFACER_REP_VIDE -#------------------- -def effacer_rep_vide(rep): - """ - Pour supprimer un repertoire et ses sous-repertoires s'ils ne contiennent - aucun fichier. - """ - #TODO: voir si on peut simplifier ce code. - for sous_rep in rep.dirs(): - if len(sous_rep.dirs()) == 0 and len(sous_rep.files()) == 0: - sous_rep.rmdir() - else: effacer_rep_vide(sous_rep) - if len(rep.dirs()) == 0 and len(rep.files()) == 0: - rep.rmdir() - - -#------------------------------------------------------------------------------ -# chemin_relatif_incorrect -#-------------------------- - -def chemin_relatif_incorrect (chemin): - """ - Vérifie si le chemin relatif de fichier indiqué est incorrect, par exemple - s'il s'agit d'un chemin absolu, s'il contient "..", un signe "tilde" ou une - lettre de lecteur MS-DOS/Windows, etc... - - /,\\ : début d'un chemin absolu sous Unix, Windows. - C:xxx : lettre de lecteur MS-DOS/Windows - .. : répertoire parent (risque de "directory traversal") - ~ : indique répertoire utilisateur sous Unix - %xyz% : variable d'environnement sous Windows (désactivé actuellement) - $xyz : variable d'environnement sous Unix - """ - # est-ce un chemin absolu qui commence par "/", "\", ou ":" ? - for c in ['/', '\\']: - if chemin.startswith(c): - return True - # TODO: sur Macintosh c'est l'inverse, un chemin relatif commence par ':'. - # Est-ce un chemin absolu pour Windows avec une lettre de lecteur ? - if len(chemin)>=2 and chemin[0].isalpha() and chemin[1]==":": - return True - # est-ce qu'il contient ".." ou un tilde ? - #for c in ['..', '~', '%', '$']: - for c in ['..', '~', '$']: - if c in chemin: - return True - #TODO: ajouter detection de variable d'environnement Windows %xyz%, mais - # attention aux faux positifs. Utiliser une regex intelligente ou bien - # verifier avec la liste des variables d'environnement du systeme ? - #TODO: vérifier si codage unicode, ou autre ?? - # Sinon c'est OK, le chemin est valide: - return False - - -def getTempBase (): - """ - returns the base directory for temporary files and dirs (absolute path). - If not defined by the policy, or defined as "auto", it will be a - subdirectory named "xf" in the default system temp dir. - Else it will be the one defined in the policy. - The directory is created if it does not exist. - """ - try: - dir=politique.parametres['rep_temp'].valeur - except: - dir='auto' - if dir == 'auto': - dir = os.path.join(tempfile.gettempdir(), 'xf') - dir = os.path.abspath(dir) - Journal.debug('temp base directory: %s' % dir) - # create the dir if it doesn't exist: - if not os.path.exists(dir): - Journal.debug('creating dir %s' % dir) - os.makedirs(dir) - return dir - - -def newTempFile (suffix="", prefix=tempfile.template, text=False): - """ - creates a new temporary file using tempfilemgr.newTempFile, in the directory - specified in the policy. - returns a tuple: (file object, file name) - """ - f, fname = tempfilemgr.newTempFile(suffix=suffix, prefix=prefix, - dir=getTempBase(), text=text) - Journal.debug('new temp file: %s' % fname) - return f, fname - - -def newTempFilename (suffix="", prefix=tempfile.template, text=False): - """ - creates a new temporary file using tempfilemgr.newTempFile, in the directory - specified in the policy. The file is closed afterwards, with a null size. - returns only the file name. - """ - f, fname = newTempFile(suffix=suffix, prefix=prefix, text=text) - f.close() - return fname - - -def newTempDir (suffix="", prefix=tempfile.template): - """ - creates a new temporary directory using tempfilemgr.newTempDir - returns a str: absolute pathname of the new directory - """ - dirname = tempfilemgr.newTempDir(suffix=suffix, prefix=prefix, - dir=getTempBase()) - Journal.debug('new temp dir: %s' % dirname) - return dirname - - -# set atexit to delete all temp files when the application terminates: -tempfilemgr.set_atexit_deleteall() - - -#------------------------------------------------------------------------------ -# TESTS -#--------------------- -# tests de certaines fonctions si le module est lancé directement -# par exemple: python commun.py -if __name__ == "__main__": - #TODO: a tester sous Linux, MacOSX, BSD - print 'Tests du module "%s" :' % __file__ - print '' - print_console('print_console avec accents: éèêëçà') - print '' - - plx._test_Popen_timer() - - - +#!/usr/bin/python +# -*- coding: iso-8859-1 -*- +""" +ExeFilter - Fonctions, constantes et variables communes à tous les modules. + +Ce fichier fait partie du projet ExeFilter. +URL du projet: U{http://www.decalage.info/exefilter} + +@author: U{Philippe Lagadec} +@author: U{Arnaud Kerréneur} +@organization: DGA/CELAR + +@contact: U{Philippe Lagadec} + +@copyright: DGA/CELAR 2004-2008 +@copyright: NATO/NC3A 2008-2010 (modifications PL apres ExeFilter v1.1.0) + +@license: CeCILL (open-source compatible GPL) - cf. code source ou fichier LICENCE.txt joint + +@version: 1.06 +@status: beta + +@var MODE_DEBUG: Activation du mode debug +""" +__docformat__ = 'epytext en' + +#__author__ = "Philippe Lagadec, Arnaud Kerréneur (DGA/CELAR)" +__date__ = "2011-04-17" +__version__ = "1.06" + + +#------------------------------------------------------------------------------ +# LICENCE: + +# Copyright DGA/CELAR 2004-2008 +# Copyright NATO/NC3A 2008-2010 (modifications PL apres ExeFilter v1.1.0) +# +# Auteurs: +# - Philippe Lagadec (PL) - philippe.lagadec(a)laposte.net +# - Arnaud Kerréneur (AK) - arnaud.kerreneur(a)dga.defense.gouv.fr +# - Tanguy Vinceleux (TV) - tanguy.vinceleux(a)dga.defense.gouv.fr +# +# Ce logiciel est régi par la licence CeCILL soumise au droit français et +# respectant les principes de diffusion des logiciels libres. Vous pouvez +# utiliser, modifier et/ou redistribuer ce programme sous les conditions +# de la licence CeCILL telle que diffusée par le CEA, le CNRS et l'INRIA +# sur le site "http://www.cecill.info". Une copie de cette licence est jointe +# dans les fichiers Licence_CeCILL_V2-fr.html et Licence_CeCILL_V2-en.html. +# +# En contrepartie de l'accessibilité au code source et des droits de copie, +# de modification et de redistribution accordés par cette licence, il n'est +# offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, +# seule une responsabilité restreinte pèse sur l'auteur du programme, le +# titulaire des droits patrimoniaux et les concédants successifs. +# +# A cet égard l'attention de l'utilisateur est attirée sur les risques +# associés au chargement, à l'utilisation, à la modification et/ou au +# développement et à la reproduction du logiciel par l'utilisateur étant +# donné sa spécificité de logiciel libre, qui peut le rendre complexe à +# manipuler et qui le réserve donc à des développeurs et des professionnels +# avertis possédant des connaissances informatiques approfondies. Les +# utilisateurs sont donc invités à charger et tester l'adéquation du +# logiciel à leurs besoins dans des conditions permettant d'assurer la +# sécurité de leurs systèmes et ou de leurs données et, plus généralement, +# à l'utiliser et l'exploiter dans les mêmes conditions de sécurité. +# +# Le fait que vous puissiez accéder à cet en-tête signifie que vous avez +# pris connaissance de la licence CeCILL, et que vous en avez accepté les +# termes. + + +#------------------------------------------------------------------------------ +# HISTORIQUE: +# 2004-10-24 v0.01 PL: - 1ère version +# 2004-2006 PL,AK: - nombreuses evolutions +# 2007-01-12 v1.00 PL: - version 1.00 officielle +# 2007-07-24 PL: - import plx pour ameliorer la portabilite +# 2007-09-10 v1.01 PL: - ajout licence CeCILL +# - conversion tabs en espaces +# - amelioration imports +# - ajout de display_html_file +# 2010-02-04 v1.02 PL: - disabled sous_rep_temp to avoid race conditions +# 2010-02-07 v1.03 PL: - updated path module import +# 2010-02-23 v1.04 PL: - updated plx import +# 2011-02-18 v1.05 PL: - added getTempBase, newTempFile and newTempDir +# 2011-04-17 v1.06 PL: - added global for clean mode + +#------------------------------------------------------------------------------ +# TODO: +# + replace global variables by threading.local variables to support +# multithreading? +# - ajouter parametres pour le comportement de chemin_relatif_incorrect() +# - chemin_relatif_incorrect(): ajout verif variable environnement Windows %xy% +# ? chemin_relatif_incorrect(): ajout verif chemin relatif MacOS avec ':' ? + +#------------------------------------------------------------------------------ + +#=== IMPORTS ================================================================== + +# import du module path.py pour simplifier la gestion des fichiers/repertoires: +try: + from thirdparty.path.path import path +except: + raise ImportError("the path module is not installed: " + "see http://pypi.python.org/pypi/path.py") + +# module plx pour ameliorer la portabilite +try: + import thirdparty.plx.plx as plx +except: + raise ImportError("the plx module is not installed: " + +"see http://www.decalage.info/python/plx/") + +import tempfile, os +import thirdparty.tempfilemgr.tempfilemgr as tempfilemgr + +# modules du projet +import Journal + +#=== CONSTANTES =============================================================== + +MODE_DEBUG = False # contrôle si les messages de debug() s'affichent + +#TODO: a remplacer par attributs de la classe ExeFilter: +nb_fichiers = compteur_avancement = 0 +continuer_transfert = True +transfert_commence = False +##sous_rep_temp = None +sous_rep_archive = None +politique = None +clean_mode = True # indicates if scan-only or clean mode + + +#=== FONCTIONS ================================================================ + +# fonctions et constantes importees du module plx: +get_username = plx.get_username +display_html_file = plx.display_html_file +main_is_frozen = plx.main_is_frozen +get_main_dir = plx.get_main_dir +print_console = plx.print_console +print_oem = plx.print_console +str_console = plx.str_console +str_oem = plx.str_console +str_lat1 = plx.str_lat1 +unistr = plx.unistr +Popen_timer = plx.Popen_timer +EXIT_KILL_PTIMER = plx.EXIT_KILL_PTIMER + + +#------------------------------------------------------------------------------ +# mode_debug +#------------------- + +def mode_debug(mode=None): + """ + Pour activer ou désactiver le mode debug, en modifiant la variable + globale MODE_DEBUG. Retourne la valeur de cette variable. + """ + global MODE_DEBUG + if mode!=None: + MODE_DEBUG = bool(mode) + return MODE_DEBUG + + +#------------------------------------------------------------------------------ +# DEBUG +#------------------- +def debug(texte): + """ + pour afficher un texte de débogage si MODE_DEBUG = True + + @param texte: le texte à afficher + @type texte: str + + """ + if MODE_DEBUG: + Journal.debug(texte) + #print_oem ("DEBUG: " + texte) + + +#------------------------------------------------------------------------------ +# DEBUG_PAUSE +#------------------- +def debug_pause(texte=None): + """ + pour faire une pause si MODE_DEBUG = True, et laisser l'utilisateur + lire les messages à l'écran. + """ + if MODE_DEBUG: + if texte: debug(texte) + #debug("Appuyer sur Entree pour continuer...") + print("Appuyer sur Entree pour continuer, ou Ctrl+C pour stopper...") + attente = input() + + +#------------------------------------------------------------------------------ +# EFFACER_REP_VIDE +#------------------- +def effacer_rep_vide(rep): + """ + Pour supprimer un repertoire et ses sous-repertoires s'ils ne contiennent + aucun fichier. + """ + #TODO: voir si on peut simplifier ce code. + for sous_rep in rep.dirs(): + if len(sous_rep.dirs()) == 0 and len(sous_rep.files()) == 0: + sous_rep.rmdir() + else: effacer_rep_vide(sous_rep) + if len(rep.dirs()) == 0 and len(rep.files()) == 0: + rep.rmdir() + + +#------------------------------------------------------------------------------ +# chemin_relatif_incorrect +#-------------------------- + +def chemin_relatif_incorrect (chemin): + """ + Vérifie si le chemin relatif de fichier indiqué est incorrect, par exemple + s'il s'agit d'un chemin absolu, s'il contient "..", un signe "tilde" ou une + lettre de lecteur MS-DOS/Windows, etc... + + /,\\ : début d'un chemin absolu sous Unix, Windows. + C:xxx : lettre de lecteur MS-DOS/Windows + .. : répertoire parent (risque de "directory traversal") + ~ : indique répertoire utilisateur sous Unix + %xyz% : variable d'environnement sous Windows (désactivé actuellement) + $xyz : variable d'environnement sous Unix + """ + # est-ce un chemin absolu qui commence par "/", "\", ou ":" ? + for c in ['/', '\\']: + if chemin.startswith(c): + return True + # TODO: sur Macintosh c'est l'inverse, un chemin relatif commence par ':'. + # Est-ce un chemin absolu pour Windows avec une lettre de lecteur ? + if len(chemin)>=2 and chemin[0].isalpha() and chemin[1]==":": + return True + # est-ce qu'il contient ".." ou un tilde ? + #for c in ['..', '~', '%', '$']: + for c in ['..', '~', '$']: + if c in chemin: + return True + #TODO: ajouter detection de variable d'environnement Windows %xyz%, mais + # attention aux faux positifs. Utiliser une regex intelligente ou bien + # verifier avec la liste des variables d'environnement du systeme ? + #TODO: vérifier si codage unicode, ou autre ?? + # Sinon c'est OK, le chemin est valide: + return False + + +def getTempBase (): + """ + returns the base directory for temporary files and dirs (absolute path). + If not defined by the policy, or defined as "auto", it will be a + subdirectory named "xf" in the default system temp dir. + Else it will be the one defined in the policy. + The directory is created if it does not exist. + """ + try: + dir=politique.parametres['rep_temp'].valeur + except: + dir='auto' + if dir == 'auto': + dir = os.path.join(tempfile.gettempdir(), 'xf') + dir = os.path.abspath(dir) + Journal.debug('temp base directory: %s' % dir) + # create the dir if it doesn't exist: + if not os.path.exists(dir): + Journal.debug('creating dir %s' % dir) + os.makedirs(dir) + return dir + + +def newTempFile (suffix="", prefix=tempfile.template, text=False): + """ + creates a new temporary file using tempfilemgr.newTempFile, in the directory + specified in the policy. + returns a tuple: (file object, file name) + """ + f, fname = tempfilemgr.newTempFile(suffix=suffix, prefix=prefix, + dir=getTempBase(), text=text) + Journal.debug('new temp file: %s' % fname) + return f, fname + + +def newTempFilename (suffix="", prefix=tempfile.template, text=False): + """ + creates a new temporary file using tempfilemgr.newTempFile, in the directory + specified in the policy. The file is closed afterwards, with a null size. + returns only the file name. + """ + f, fname = newTempFile(suffix=suffix, prefix=prefix, text=text) + f.close() + return fname + + +def newTempDir (suffix="", prefix=tempfile.template): + """ + creates a new temporary directory using tempfilemgr.newTempDir + returns a str: absolute pathname of the new directory + """ + dirname = tempfilemgr.newTempDir(suffix=suffix, prefix=prefix, + dir=getTempBase()) + Journal.debug('new temp dir: %s' % dirname) + return dirname + + +# set atexit to delete all temp files when the application terminates: +tempfilemgr.set_atexit_deleteall() + + +#------------------------------------------------------------------------------ +# TESTS +#--------------------- +# tests de certaines fonctions si le module est lancé directement +# par exemple: python commun.py +if __name__ == "__main__": + #TODO: a tester sous Linux, MacOSX, BSD + print('Tests du module "%s" :' % __file__) + print('') + print_console('print_console avec accents: éèêëçà') + print('') + + plx._test_Popen_timer() + + + diff --git a/thirdparty/HTML/HTML.py b/thirdparty/HTML/HTML.py index b03c0a2..0ce67e7 100644 --- a/thirdparty/HTML/HTML.py +++ b/thirdparty/HTML/HTML.py @@ -1,493 +1,489 @@ -#!/usr/bin/python -# -*- coding: iso-8859-1 -*- -""" -HTML.py - v0.04 2009-07-28 Philippe Lagadec - -This module provides a few classes to easily generate HTML code such as tables -and lists. - -Project website: http://www.decalage.info/python/html - -License: CeCILL (open-source GPL compatible), see source code for details. - http://www.cecill.info -""" - -__version__ = '0.04' -__date__ = '2009-07-28' -__author__ = 'Philippe Lagadec' - -#--- LICENSE ------------------------------------------------------------------ - -# Copyright Philippe Lagadec - see http://www.decalage.info/contact for contact info -# -# This module provides a few classes to easily generate HTML tables and lists. -# -# This software is governed by the CeCILL license under French law and -# abiding by the rules of distribution of free software. You can use, -# modify and/or redistribute the software under the terms of the CeCILL -# license as circulated by CEA, CNRS and INRIA at the following URL -# "http://www.cecill.info". -# -# A copy of the CeCILL license is also provided in these attached files: -# Licence_CeCILL_V2-en.html and Licence_CeCILL_V2-fr.html -# -# As a counterpart to the access to the source code and rights to copy, -# modify and redistribute granted by the license, users are provided only -# with a limited warranty and the software's author, the holder of the -# economic rights, and the successive licensors have only limited -# liability. -# -# In this respect, the user's attention is drawn to the risks associated -# with loading, using, modifying and/or developing or reproducing the -# software by the user in light of its specific status of free software, -# that may mean that it is complicated to manipulate, and that also -# therefore means that it is reserved for developers and experienced -# professionals having in-depth computer knowledge. Users are therefore -# encouraged to load and test the software's suitability as regards their -# requirements in conditions enabling the security of their systems and/or -# data to be ensured and, more generally, to use and operate it in the -# same conditions as regards security. -# -# The fact that you are presently reading this means that you have had -# knowledge of the CeCILL license and that you accept its terms. - - -#--- CHANGES ------------------------------------------------------------------ - -# 2008-10-06 v0.01 PL: - First version -# 2008-10-13 v0.02 PL: - added cellspacing and cellpadding to table -# - added functions to ease one-step creation of tables -# and lists -# 2009-07-21 v0.03 PL: - added column attributes and styles (first attempt) -# (thanks to an idea submitted by Michal Cernoevic) -# 2009-07-28 v0.04 PL: - improved column styles, workaround for Mozilla - - -#------------------------------------------------------------------------------- -#TODO: -# - method to return a generator (yield each row) instead of a single string -# - unicode support (input and output) -# - escape text in cells (optional) -# - constants for standard colors -# - use lxml to generate well-formed HTML ? -# - add classes/functions to generate a HTML page, paragraphs, headings, etc... - - -#--- THANKS -------------------------------------------------------------------- - -# - Michal Cernoevic, for the idea of column styles. - -#--- REFERENCES ---------------------------------------------------------------- - -# HTML 4.01 specs: http://www.w3.org/TR/html4/struct/tables.html - -# Colors: http://www.w3.org/TR/html4/types.html#type-color - -# Columns alignement and style, one of the oldest and trickiest bugs in Mozilla: -# https://bugzilla.mozilla.org/show_bug.cgi?id=915 - - -#--- CONSTANTS ----------------------------------------------------------------- - -# Table style to get thin black lines in Mozilla/Firefox instead of 3D borders -TABLE_STYLE_THINBORDER = "border: 1px solid #000000; border-collapse: collapse;" -#TABLE_STYLE_THINBORDER = "border: 1px solid #000000;" - - -#=== CLASSES =================================================================== - -class TableCell (object): - """ - a TableCell object is used to create a cell in a HTML table. (TD or TH) - - Attributes: - - text: text in the cell (may contain HTML tags). May be any object which - can be converted to a string using str(). - - header: bool, false for a normal data cell (TD), true for a header cell (TH) - - bgcolor: str, background color - - width: str, width - - align: str, horizontal alignement (left, center, right, justify or char) - - char: str, alignment character, decimal point if not specified - - charoff: str, see HTML specs - - valign: str, vertical alignment (top|middle|bottom|baseline) - - style: str, CSS style - - attribs: dict, additional attributes for the TD/TH tag - - Reference: http://www.w3.org/TR/html4/struct/tables.html#h-11.2.6 - """ - - def __init__(self, text="", bgcolor=None, header=False, width=None, - align=None, char=None, charoff=None, valign=None, style=None, - attribs=None): - """TableCell constructor""" - self.text = text - self.bgcolor = bgcolor - self.header = header - self.width = width - self.align = align - self.char = char - self.charoff = charoff - self.valign = valign - self.style = style - self.attribs = attribs - if attribs==None: - self.attribs = {} - - def __str__(self): - """return the HTML code for the table cell as a string""" - attribs_str = "" - if self.bgcolor: self.attribs['bgcolor'] = self.bgcolor - if self.width: self.attribs['width'] = self.width - if self.align: self.attribs['align'] = self.align - if self.char: self.attribs['char'] = self.char - if self.charoff: self.attribs['charoff'] = self.charoff - if self.valign: self.attribs['valign'] = self.valign - if self.style: self.attribs['style'] = self.style - for attr in self.attribs: - attribs_str += ' %s="%s"' % (attr, self.attribs[attr]) - if self.text: - text = str(self.text) - else: - # An empty cell should at least contain a non-breaking space - text = ' ' - if self.header: - return ' %s\n' % (attribs_str, text) - else: - return ' %s\n' % (attribs_str, text) - -#------------------------------------------------------------------------------- - -class TableRow (object): - """ - a TableRow object is used to create a row in a HTML table. (TR tag) - - Attributes: - - cells: list, tuple or any iterable, containing one string or TableCell - object for each cell - - header: bool, true for a header row (TH), false for a normal data row (TD) - - bgcolor: str, background color - - col_align, col_valign, col_char, col_charoff, col_styles: see Table class - - attribs: dict, additional attributes for the TR tag - - Reference: http://www.w3.org/TR/html4/struct/tables.html#h-11.2.5 - """ - - def __init__(self, cells=None, bgcolor=None, header=False, attribs=None, - col_align=None, col_valign=None, col_char=None, - col_charoff=None, col_styles=None): - """TableCell constructor""" - self.bgcolor = bgcolor - self.cells = cells - self.header = header - self.col_align = col_align - self.col_valign = col_valign - self.col_char = col_char - self.col_charoff = col_charoff - self.col_styles = col_styles - self.attribs = attribs - if attribs==None: - self.attribs = {} - - def __str__(self): - """return the HTML code for the table row as a string""" - attribs_str = "" - if self.bgcolor: self.attribs['bgcolor'] = self.bgcolor - for attr in self.attribs: - attribs_str += ' %s="%s"' % (attr, self.attribs[attr]) - result = ' \n' % attribs_str - for cell in self.cells: - col = self.cells.index(cell) # cell column index - if not isinstance(cell, TableCell): - cell = TableCell(cell, header=self.header) - # apply column alignment if specified: - if self.col_align and cell.align==None: - cell.align = self.col_align[col] - if self.col_char and cell.char==None: - cell.char = self.col_char[col] - if self.col_charoff and cell.charoff==None: - cell.charoff = self.col_charoff[col] - if self.col_valign and cell.valign==None: - cell.valign = self.col_valign[col] - # apply column style if specified: - if self.col_styles and cell.style==None: - cell.style = self.col_styles[col] - result += str(cell) - result += ' \n' - return result - -#------------------------------------------------------------------------------- - -class Table (object): - """ - a Table object is used to create a HTML table. (TABLE tag) - - Attributes: - - rows: list, tuple or any iterable, containing one iterable or TableRow - object for each row - - header_row: list, tuple or any iterable, containing the header row (optional) - - border: str or int, border width - - style: str, table style in CSS syntax (thin black borders by default) - - width: str, width of the table on the page - - attribs: dict, additional attributes for the TABLE tag - - col_width: list or tuple defining width for each column - - col_align: list or tuple defining horizontal alignment for each column - - col_char: list or tuple defining alignment character for each column - - col_charoff: list or tuple defining charoff attribute for each column - - col_valign: list or tuple defining vertical alignment for each column - - col_styles: list or tuple of HTML styles for each column - - Reference: http://www.w3.org/TR/html4/struct/tables.html#h-11.2.1 - """ - - def __init__(self, rows=None, border='1', style=None, width=None, - cellspacing=None, cellpadding=4, attribs=None, header_row=None, - col_width=None, col_align=None, col_valign=None, - col_char=None, col_charoff=None, col_styles=None): - """TableCell constructor""" - self.border = border - self.style = style - # style for thin borders by default - if style == None: self.style = TABLE_STYLE_THINBORDER - self.width = width - self.cellspacing = cellspacing - self.cellpadding = cellpadding - self.header_row = header_row - self.rows = rows - if not rows: self.rows = [] - self.attribs = attribs - if not attribs: self.attribs = {} - self.col_width = col_width - self.col_align = col_align - self.col_char = col_char - self.col_charoff = col_charoff - self.col_valign = col_valign - self.col_styles = col_styles - - def __str__(self): - """return the HTML code for the table as a string""" - attribs_str = "" - if self.border: self.attribs['border'] = self.border - if self.style: self.attribs['style'] = self.style - if self.width: self.attribs['width'] = self.width - if self.cellspacing: self.attribs['cellspacing'] = self.cellspacing - if self.cellpadding: self.attribs['cellpadding'] = self.cellpadding - for attr in self.attribs: - attribs_str += ' %s="%s"' % (attr, self.attribs[attr]) - result = '\n' % attribs_str - # insert column tags and attributes if specified: - if self.col_width: - for width in self.col_width: - result += ' \n' % width - # The following code would also generate column attributes for style - # and alignement according to HTML4 specs, - # BUT it is not supported completely (only width) on Mozilla Firefox: - # see https://bugzilla.mozilla.org/show_bug.cgi?id=915 -## n_cols = max(len(self.col_styles), len(self.col_width), -## len(self.col_align), len(self.col_valign)) -## for i in range(n_cols): -## col = '' -## try: -## if self.col_styles[i]: -## col += ' style="%s"' % self.col_styles[i] -## except: pass -## try: -## if self.col_width[i]: -## col += ' width="%s"' % self.col_width[i] -## except: pass -## try: -## if self.col_align[i]: -## col += ' align="%s"' % self.col_align[i] -## except: pass -## try: -## if self.col_valign[i]: -## col += ' valign="%s"' % self.col_valign[i] -## except: pass -## result += '\n' % col - # First insert a header row if specified: - if self.header_row: - if not isinstance(self.header_row, TableRow): - result += str(TableRow(self.header_row, header=True)) - else: - result += str(self.header_row) - # Then all data rows: - for row in self.rows: - if not isinstance(row, TableRow): - row = TableRow(row) - # apply column alignments and styles to each row if specified: - # (Mozilla bug workaround) - if self.col_align and not row.col_align: - row.col_align = self.col_align - if self.col_char and not row.col_char: - row.col_char = self.col_char - if self.col_charoff and not row.col_charoff: - row.col_charoff = self.col_charoff - if self.col_valign and not row.col_valign: - row.col_valign = self.col_valign - if self.col_styles and not row.col_styles: - row.col_styles = self.col_styles - result += str(row) - result += '' - return result - - -#------------------------------------------------------------------------------- - -class List (object): - """ - a List object is used to create an ordered or unordered list in HTML. - (UL/OL tag) - - Attributes: - - lines: list, tuple or any iterable, containing one string for each line - - ordered: bool, choice between an ordered (OL) or unordered list (UL) - - attribs: dict, additional attributes for the OL/UL tag - - Reference: http://www.w3.org/TR/html4/struct/lists.html - """ - - def __init__(self, lines=None, ordered=False, start=None, attribs=None): - """List constructor""" - if lines: - self.lines = lines - else: - self.lines = [] - self.ordered = ordered - self.start = start - if attribs: - self.attribs = attribs - else: - self.attribs = {} - - def __str__(self): - """return the HTML code for the list as a string""" - attribs_str = "" - if self.start: self.attribs['start'] = self.start - for attr in self.attribs: - attribs_str += ' %s="%s"' % (attr, self.attribs[attr]) - if self.ordered: tag = 'OL' - else: tag = 'UL' - result = '<%s%s>\n' % (tag, attribs_str) - for line in self.lines: - result += '

  • %s\n' % str(line) - result += '\n' % tag - return result - - -##class Link (object): -## """ -## a Link object is used to create link in HTML. ( tag) -## -## Attributes: -## - text: str, text of the link -## - url: str, URL of the link -## - attribs: dict, additional attributes for the A tag -## -## Reference: http://www.w3.org/TR/html4 -## """ -## -## def __init__(self, text, url=None, attribs=None): -## """Link constructor""" -## self.text = text -## self.url = url -## if attribs: -## self.attribs = attribs -## else: -## self.attribs = {} -## -## def __str__(self): -## """return the HTML code for the link as a string""" -## attribs_str = "" -## if self.url: self.attribs['href'] = self.url -## for attr in self.attribs: -## attribs_str += ' %s="%s"' % (attr, self.attribs[attr]) -## return '%s' % (attribs_str, text) - - -#=== FUNCTIONS ================================================================ - -# much simpler definition of a link as a function: -def Link(text, url): - return '%s' % (url, text) - -def link(text, url): - return '%s' % (url, text) - -def table(*args, **kwargs): - 'return HTML code for a table as a string. See Table class for parameters.' - return str(Table(*args, **kwargs)) - -def list(*args, **kwargs): - 'return HTML code for a list as a string. See List class for parameters.' - return str(List(*args, **kwargs)) - - -#=== MAIN ===================================================================== - -# Show sample usage when this file is launched as a script. - -if __name__ == '__main__': - - # open an HTML file to show output in a browser - f = open('test.html', 'w') - - t = Table() - t.rows.append(TableRow(['A', 'B', 'C'], header=True)) - t.rows.append(TableRow(['D', 'E', 'F'])) - t.rows.append(('i', 'j', 'k')) - f.write(str(t) + '

    \n') - print str(t) - print '-'*79 - - t2 = Table([ - ('1', '2'), - ['3', '4'] - ], width='100%', header_row=('col1', 'col2'), - col_width=('', '75%')) - f.write(str(t2) + '

    \n') - print t2 - print '-'*79 - - t2.rows.append(['5', '6']) - t2.rows[1][1] = TableCell('new', bgcolor='red') - t2.rows.append(TableRow(['7', '8'], attribs={'align': 'center'})) - f.write(str(t2) + '

    \n') - print t2 - print '-'*79 - - # sample table with column attributes and styles: - table_data = [ - ['Smith', 'John', 30, 4.5], - ['Carpenter', 'Jack', 47, 7], - ['Johnson', 'Paul', 62, 10.55], - ] - htmlcode = HTML.table(table_data, - header_row = ['Last name', 'First name', 'Age', 'Score'], - col_width=['', '20%', '10%', '10%'], - col_align=['left', 'center', 'right', 'char'], - col_styles=['font-size: large', '', 'font-size: small', 'background-color:yellow']) - f.write(htmlcode + '

    \n') - print htmlcode - print '-'*79 - - def gen_table_squares(n): - """ - Generator to create table rows for integers from 1 to n - """ -## # First, header row: -## yield TableRow(('x', 'square(x)'), header=True, bgcolor='blue') -## # Then all rows: - for x in range(1, n+1): - yield (x, x*x) - - t = Table(rows=gen_table_squares(10), header_row=('x', 'square(x)')) - f.write(str(t) + '

    \n') - - print '-'*79 - l = List(['aaa', 'bbb', 'ccc']) - f.write(str(l) + '

    \n') - l.ordered = True - f.write(str(l) + '

    \n') - l.start=10 - f.write(str(l) + '

    \n') - - f.close() +#!/usr/bin/python +# -*- coding: iso-8859-1 -*- +""" +HTML.py - v0.04 2009-07-28 Philippe Lagadec + +This module provides a few classes to easily generate HTML code such as tables +and lists. + +Project website: http://www.decalage.info/python/html + +License: CeCILL (open-source GPL compatible), see source code for details. + http://www.cecill.info +""" + +__version__ = '0.04' +__date__ = '2009-07-28' +__author__ = 'Philippe Lagadec' + +#--- LICENSE ------------------------------------------------------------------ + +# Copyright Philippe Lagadec - see http://www.decalage.info/contact for contact info +# +# This module provides a few classes to easily generate HTML tables and lists. +# +# This software is governed by the CeCILL license under French law and +# abiding by the rules of distribution of free software. You can use, +# modify and/or redistribute the software under the terms of the CeCILL +# license as circulated by CEA, CNRS and INRIA at the following URL +# "http://www.cecill.info". +# +# A copy of the CeCILL license is also provided in these attached files: +# Licence_CeCILL_V2-en.html and Licence_CeCILL_V2-fr.html +# +# As a counterpart to the access to the source code and rights to copy, +# modify and redistribute granted by the license, users are provided only +# with a limited warranty and the software's author, the holder of the +# economic rights, and the successive licensors have only limited +# liability. +# +# In this respect, the user's attention is drawn to the risks associated +# with loading, using, modifying and/or developing or reproducing the +# software by the user in light of its specific status of free software, +# that may mean that it is complicated to manipulate, and that also +# therefore means that it is reserved for developers and experienced +# professionals having in-depth computer knowledge. Users are therefore +# encouraged to load and test the software's suitability as regards their +# requirements in conditions enabling the security of their systems and/or +# data to be ensured and, more generally, to use and operate it in the +# same conditions as regards security. +# +# The fact that you are presently reading this means that you have had +# knowledge of the CeCILL license and that you accept its terms. + + +#--- CHANGES ------------------------------------------------------------------ + +# 2008-10-06 v0.01 PL: - First version +# 2008-10-13 v0.02 PL: - added cellspacing and cellpadding to table +# - added functions to ease one-step creation of tables +# and lists +# 2009-07-21 v0.03 PL: - added column attributes and styles (first attempt) +# (thanks to an idea submitted by Michal Cernoevic) +# 2009-07-28 v0.04 PL: - improved column styles, workaround for Mozilla + + +#------------------------------------------------------------------------------- +#TODO: +# - method to return a generator (yield each row) instead of a single string +# - unicode support (input and output) +# - escape text in cells (optional) +# - constants for standard colors +# - use lxml to generate well-formed HTML ? +# - add classes/functions to generate a HTML page, paragraphs, headings, etc... + + +#--- THANKS -------------------------------------------------------------------- + +# - Michal Cernoevic, for the idea of column styles. + +#--- REFERENCES ---------------------------------------------------------------- + +# HTML 4.01 specs: http://www.w3.org/TR/html4/struct/tables.html + +# Colors: http://www.w3.org/TR/html4/types.html#type-color + +# Columns alignement and style, one of the oldest and trickiest bugs in Mozilla: +# https://bugzilla.mozilla.org/show_bug.cgi?id=915 + + +#--- CONSTANTS ----------------------------------------------------------------- + +# Table style to get thin black lines in Mozilla/Firefox instead of 3D borders +TABLE_STYLE_THINBORDER = "border: 1px solid #000000; border-collapse: collapse;" +#TABLE_STYLE_THINBORDER = "border: 1px solid #000000;" + + +#=== CLASSES =================================================================== + +class TableCell (object): + """ + a TableCell object is used to create a cell in a HTML table. (TD or TH) + + Attributes: + - text: text in the cell (may contain HTML tags). May be any object which + can be converted to a string using str(). + - header: bool, false for a normal data cell (TD), true for a header cell (TH) + - bgcolor: str, background color + - width: str, width + - align: str, horizontal alignement (left, center, right, justify or char) + - char: str, alignment character, decimal point if not specified + - charoff: str, see HTML specs + - valign: str, vertical alignment (top|middle|bottom|baseline) + - style: str, CSS style + - attribs: dict, additional attributes for the TD/TH tag + + Reference: http://www.w3.org/TR/html4/struct/tables.html#h-11.2.6 + """ + + def __init__(self, text="", bgcolor=None, header=False, width=None, + align=None, char=None, charoff=None, valign=None, style=None, + attribs=None): + """TableCell constructor""" + self.text = text + self.bgcolor = bgcolor + self.header = header + self.width = width + self.align = align + self.char = char + self.charoff = charoff + self.valign = valign + self.style = style + self.attribs = attribs + if attribs==None: + self.attribs = {} + + def __str__(self): + """return the HTML code for the table cell as a string""" + attribs_str = "" + if self.bgcolor: self.attribs['bgcolor'] = self.bgcolor + if self.width: self.attribs['width'] = self.width + if self.align: self.attribs['align'] = self.align + if self.char: self.attribs['char'] = self.char + if self.charoff: self.attribs['charoff'] = self.charoff + if self.valign: self.attribs['valign'] = self.valign + if self.style: self.attribs['style'] = self.style + for attr in self.attribs: + attribs_str += ' %s="%s"' % (attr, self.attribs[attr]) + if self.text: + text = str(self.text) + else: + # An empty cell should at least contain a non-breaking space + text = ' ' + if self.header: + return ' %s\n' % (attribs_str, text) + else: + return ' %s\n' % (attribs_str, text) + +#------------------------------------------------------------------------------- + +class TableRow (object): + """ + a TableRow object is used to create a row in a HTML table. (TR tag) + + Attributes: + - cells: list, tuple or any iterable, containing one string or TableCell + object for each cell + - header: bool, true for a header row (TH), false for a normal data row (TD) + - bgcolor: str, background color + - col_align, col_valign, col_char, col_charoff, col_styles: see Table class + - attribs: dict, additional attributes for the TR tag + + Reference: http://www.w3.org/TR/html4/struct/tables.html#h-11.2.5 + """ + + def __init__(self, cells=None, bgcolor=None, header=False, attribs=None, + col_align=None, col_valign=None, col_char=None, + col_charoff=None, col_styles=None): + """TableCell constructor""" + self.bgcolor = bgcolor + self.cells = cells + self.header = header + self.col_align = col_align + self.col_valign = col_valign + self.col_char = col_char + self.col_charoff = col_charoff + self.col_styles = col_styles + self.attribs = attribs + if attribs==None: + self.attribs = {} + + def __str__(self): + """return the HTML code for the table row as a string""" + attribs_str = "" + if self.bgcolor: self.attribs['bgcolor'] = self.bgcolor + for attr in self.attribs: + attribs_str += ' %s="%s"' % (attr, self.attribs[attr]) + result = ' \n' % attribs_str + for cell in self.cells: + col = self.cells.index(cell) # cell column index + if not isinstance(cell, TableCell): + cell = TableCell(cell, header=self.header) + # apply column alignment if specified: + if self.col_align and cell.align==None: + cell.align = self.col_align[col] + if self.col_char and cell.char==None: + cell.char = self.col_char[col] + if self.col_charoff and cell.charoff==None: + cell.charoff = self.col_charoff[col] + if self.col_valign and cell.valign==None: + cell.valign = self.col_valign[col] + # apply column style if specified: + if self.col_styles and cell.style==None: + cell.style = self.col_styles[col] + result += str(cell) + result += ' \n' + return result + +#------------------------------------------------------------------------------- + +class Table (object): + """ + a Table object is used to create a HTML table. (TABLE tag) + + Attributes: + - rows: list, tuple or any iterable, containing one iterable or TableRow + object for each row + - header_row: list, tuple or any iterable, containing the header row (optional) + - border: str or int, border width + - style: str, table style in CSS syntax (thin black borders by default) + - width: str, width of the table on the page + - attribs: dict, additional attributes for the TABLE tag + - col_width: list or tuple defining width for each column + - col_align: list or tuple defining horizontal alignment for each column + - col_char: list or tuple defining alignment character for each column + - col_charoff: list or tuple defining charoff attribute for each column + - col_valign: list or tuple defining vertical alignment for each column + - col_styles: list or tuple of HTML styles for each column + + Reference: http://www.w3.org/TR/html4/struct/tables.html#h-11.2.1 + """ + + def __init__(self, rows=None, border='1', style=None, width=None, + cellspacing=None, cellpadding=4, attribs=None, header_row=None, + col_width=None, col_align=None, col_valign=None, + col_char=None, col_charoff=None, col_styles=None): + """TableCell constructor""" + self.border = border + self.style = style + # style for thin borders by default + if style == None: self.style = TABLE_STYLE_THINBORDER + self.width = width + self.cellspacing = cellspacing + self.cellpadding = cellpadding + self.header_row = header_row + self.rows = rows + if not rows: self.rows = [] + self.attribs = attribs + if not attribs: self.attribs = {} + self.col_width = col_width + self.col_align = col_align + self.col_char = col_char + self.col_charoff = col_charoff + self.col_valign = col_valign + self.col_styles = col_styles + + def __str__(self): + """return the HTML code for the table as a string""" + attribs_str = "" + if self.border: self.attribs['border'] = self.border + if self.style: self.attribs['style'] = self.style + if self.width: self.attribs['width'] = self.width + if self.cellspacing: self.attribs['cellspacing'] = self.cellspacing + if self.cellpadding: self.attribs['cellpadding'] = self.cellpadding + for attr in self.attribs: + attribs_str += ' %s="%s"' % (attr, self.attribs[attr]) + result = '\n' % attribs_str + # insert column tags and attributes if specified: + if self.col_width: + for width in self.col_width: + result += ' \n' % width + # The following code would also generate column attributes for style + # and alignement according to HTML4 specs, + # BUT it is not supported completely (only width) on Mozilla Firefox: + # see https://bugzilla.mozilla.org/show_bug.cgi?id=915 +## n_cols = max(len(self.col_styles), len(self.col_width), +## len(self.col_align), len(self.col_valign)) +## for i in range(n_cols): +## col = '' +## try: +## if self.col_styles[i]: +## col += ' style="%s"' % self.col_styles[i] +## except: pass +## try: +## if self.col_width[i]: +## col += ' width="%s"' % self.col_width[i] +## except: pass +## try: +## if self.col_align[i]: +## col += ' align="%s"' % self.col_align[i] +## except: pass +## try: +## if self.col_valign[i]: +## col += ' valign="%s"' % self.col_valign[i] +## except: pass +## result += '\n' % col + # First insert a header row if specified: + if self.header_row: + if not isinstance(self.header_row, TableRow): + result += str(TableRow(self.header_row, header=True)) + else: + result += str(self.header_row) + # Then all data rows: + for row in self.rows: + if not isinstance(row, TableRow): + row = TableRow(row) + # apply column alignments and styles to each row if specified: + # (Mozilla bug workaround) + if self.col_align and not row.col_align: + row.col_align = self.col_align + if self.col_char and not row.col_char: + row.col_char = self.col_char + if self.col_charoff and not row.col_charoff: + row.col_charoff = self.col_charoff + if self.col_valign and not row.col_valign: + row.col_valign = self.col_valign + if self.col_styles and not row.col_styles: + row.col_styles = self.col_styles + result += str(row) + result += '' + return result + + +#------------------------------------------------------------------------------- + +class List (object): + """ + a List object is used to create an ordered or unordered list in HTML. + (UL/OL tag) + + Attributes: + - lines: list, tuple or any iterable, containing one string for each line + - ordered: bool, choice between an ordered (OL) or unordered list (UL) + - attribs: dict, additional attributes for the OL/UL tag + + Reference: http://www.w3.org/TR/html4/struct/lists.html + """ + + def __init__(self, lines=None, ordered=False, start=None, attribs=None): + """List constructor""" + if lines: + self.lines = lines + else: + self.lines = [] + self.ordered = ordered + self.start = start + if attribs: + self.attribs = attribs + else: + self.attribs = {} + + def __str__(self): + """return the HTML code for the list as a string""" + attribs_str = "" + if self.start: self.attribs['start'] = self.start + for attr in self.attribs: + attribs_str += ' %s="%s"' % (attr, self.attribs[attr]) + if self.ordered: tag = 'OL' + else: tag = 'UL' + result = '<%s%s>\n' % (tag, attribs_str) + for line in self.lines: + result += '

  • %s\n' % str(line) + result += '\n' % tag + return result + + +##class Link (object): +## """ +## a Link object is used to create link in HTML. ( tag) +## +## Attributes: +## - text: str, text of the link +## - url: str, URL of the link +## - attribs: dict, additional attributes for the A tag +## +## Reference: http://www.w3.org/TR/html4 +## """ +## +## def __init__(self, text, url=None, attribs=None): +## """Link constructor""" +## self.text = text +## self.url = url +## if attribs: +## self.attribs = attribs +## else: +## self.attribs = {} +## +## def __str__(self): +## """return the HTML code for the link as a string""" +## attribs_str = "" +## if self.url: self.attribs['href'] = self.url +## for attr in self.attribs: +## attribs_str += ' %s="%s"' % (attr, self.attribs[attr]) +## return '%s' % (attribs_str, text) + + +#=== FUNCTIONS ================================================================ + +# much simpler definition of a link as a function: +def Link(text, url): + return '%s' % (url, text) + +def link(text, url): + return '%s' % (url, text) + +def table(*args, **kwargs): + 'return HTML code for a table as a string. See Table class for parameters.' + return str(Table(*args, **kwargs)) + +def list(*args, **kwargs): + 'return HTML code for a list as a string. See List class for parameters.' + return str(List(*args, **kwargs)) + + +#=== MAIN ===================================================================== + +# Show sample usage when this file is launched as a script. + +if __name__ == '__main__': + + # open an HTML file to show output in a browser + f = open('test.html', 'w') + + t = Table() + t.rows.append(TableRow(['A', 'B', 'C'], header=True)) + t.rows.append(TableRow(['D', 'E', 'F'])) + t.rows.append(('i', 'j', 'k')) + f.write(str(t) + '

    \n') + print(str(t)) + print("-"*79) + t2 = Table([ + ('1', '2'), + ['3', '4'] + ], width='100%', header_row=('col1', 'col2'), + col_width=('', '75%')) + f.write(str(t2) + '

    \n') + print(t2) + print("-"*79) + t2.rows.append(['5', '6']) + t2.rows[1][1] = TableCell('new', bgcolor='red') + t2.rows.append(TableRow(['7', '8'], attribs={'align': 'center'})) + f.write(str(t2) + '

    \n') + print(t2) + print("-"*79) + # sample table with column attributes and styles: + table_data = [ + ['Smith', 'John', 30, 4.5], + ['Carpenter', 'Jack', 47, 7], + ['Johnson', 'Paul', 62, 10.55], + ] + htmlcode = HTML.table(table_data, + header_row = ['Last name', 'First name', 'Age', 'Score'], + col_width=['', '20%', '10%', '10%'], + col_align=['left', 'center', 'right', 'char'], + col_styles=['font-size: large', '', 'font-size: small', 'background-color:yellow']) + f.write(htmlcode + '

    \n') + print(htmlcode) + print("-"*79) + def gen_table_squares(n): + """ + Generator to create table rows for integers from 1 to n + """ +## # First, header row: +## yield TableRow(('x', 'square(x)'), header=True, bgcolor='blue') +## # Then all rows: + for x in range(1, n+1): + yield (x, x*x) + + t = Table(rows=gen_table_squares(10), header_row=('x', 'square(x)')) + f.write(str(t) + '

    \n') + + print("-"*79) + l = List(['aaa', 'bbb', 'ccc']) + f.write(str(l) + '

    \n') + l.ordered = True + f.write(str(l) + '

    \n') + l.start=10 + f.write(str(l) + '

    \n') + + f.close() diff --git a/thirdparty/HTML/HTML_tutorial.py b/thirdparty/HTML/HTML_tutorial.py index bf01dde..2ce8dea 100644 --- a/thirdparty/HTML/HTML_tutorial.py +++ b/thirdparty/HTML/HTML_tutorial.py @@ -1,208 +1,185 @@ -# HTML.py tutorial - P. Lagadec - -# see also http://www.decalage.info/en/python/html for more details and -# updates. - - -import HTML - -# open an HTML file to show output in a browser -HTMLFILE = 'HTML_tutorial_output.html' -f = open(HTMLFILE, 'w') - - -#=== TABLES =================================================================== - -# 1) a simple HTML table may be built from a list of lists: - -table_data = [ - ['Last name', 'First name', 'Age'], - ['Smith', 'John', 30], - ['Carpenter', 'Jack', 47], - ['Johnson', 'Paul', 62], - ] - -htmlcode = HTML.table(table_data) -print htmlcode -f.write(htmlcode) -f.write('

    ') -print '-'*79 - -#------------------------------------------------------------------------------- - -# 2) a header row may be specified: it will appear in bold in browsers - -table_data = [ - ['Smith', 'John', 30], - ['Carpenter', 'Jack', 47], - ['Johnson', 'Paul', 62], - ] - -htmlcode = HTML.table(table_data, - header_row=['Last name', 'First name', 'Age']) -print htmlcode -f.write(htmlcode) -f.write('

    ') -print '-'*79 - - -#------------------------------------------------------------------------------- - -# 3) you may also create a Table object and add rows one by one: - -t = HTML.Table(header_row=['x', 'square(x)', 'cube(x)']) -for x in range(1,10): - t.rows.append([x, x*x, x*x*x]) -htmlcode = str(t) -print htmlcode -f.write(htmlcode) -f.write('

    ') -print '-'*79 - - -#------------------------------------------------------------------------------- - -# 4) rows may be any iterable (list, tuple, ...) including a generator: -# (this is useful to save memory when generating a large table) - -def gen_rows(i): - 'rows generator' - for x in range(1,i): - yield [x, x*x, x*x*x] - -htmlcode = HTML.table(gen_rows(10), header_row=['x', 'square(x)', 'cube(x)']) -print htmlcode -f.write(htmlcode) -f.write('

    ') -print '-'*79 - - -#------------------------------------------------------------------------------- - -# 5) to choose a specific background color for a cell, use a TableCell -# object: - -HTML_COLORS = ['Black', 'Green', 'Silver', 'Lime', 'Gray', 'Olive', 'White', - 'Maroon', 'Navy', 'Red', 'Blue', 'Purple', 'Teal', 'Fuchsia', 'Aqua'] - -t = HTML.Table(header_row=['Name', 'Color']) -for colorname in HTML_COLORS: - colored_cell = HTML.TableCell(' ', bgcolor=colorname) - t.rows.append([colorname, colored_cell]) -htmlcode = str(t) -print htmlcode -f.write(htmlcode) -f.write('

    ') -print '-'*79 - - -#------------------------------------------------------------------------------- - -# 6) A simple way to generate a test report: - -# dictionary of test results, indexed by test id: -test_results = { - 'test 1': 'success', - 'test 2': 'failure', - 'test 3': 'success', - 'test 4': 'error', - } - -# dict of colors for each result: -result_colors = { - 'success': 'lime', - 'failure': 'red', - 'error': 'yellow', - } - -t = HTML.Table(header_row=['Test', 'Result']) -for test_id in sorted(test_results): - # create the colored cell: - color = result_colors[test_results[test_id]] - colored_result = HTML.TableCell(test_results[test_id], bgcolor=color) - # append the row with two cells: - t.rows.append([test_id, colored_result]) -htmlcode = str(t) -print htmlcode -f.write(htmlcode) -f.write('

    ') -print '-'*79 - -#------------------------------------------------------------------------------- - -# 7) sample table with column attributes and styles: -table_data = [ - ['Smith', 'John', 30, 4.5], - ['Carpenter', 'Jack', 47, 7], - ['Johnson', 'Paul', 62, 10.55], - ] -htmlcode = HTML.table(table_data, - header_row = ['Last name', 'First name', 'Age', 'Score'], - col_width=['', '20%', '10%', '10%'], - col_align=['left', 'center', 'right', 'char'], - col_styles=['font-size: large', '', 'font-size: small', 'background-color:yellow']) -f.write(htmlcode + '

    \n') -print htmlcode -print '-'*79 - - - -#=== LISTS =================================================================== - -# 1) a HTML list (with bullets) may be built from a Python list of strings: - -a_list = ['john', 'paul', 'jack'] -htmlcode = HTML.list(a_list) -print htmlcode -f.write(htmlcode) -f.write('

    ') -print '-'*79 - - -# 2) it is easy to change it into a numbered (ordered) list: - -htmlcode = HTML.list(a_list, ordered=True) -print htmlcode -f.write(htmlcode) -f.write('

    ') -print '-'*79 - - -# 3) Lines of a list may also be added one by one, when using the List class: - -html_list = HTML.List() -for i in range(1,10): - html_list.lines.append('square(%d) = %d' % (i, i*i)) -htmlcode = str(html_list) -print htmlcode -f.write(htmlcode) -f.write('

    ') -print '-'*79 - - -# 4) To save memory, a large list may be built from a generator: - -def gen_lines(i): - 'lines generator' - for x in range(1,i): - yield 'square(%d) = %d' % (x, x*x) -htmlcode = HTML.list(gen_lines(10)) -print htmlcode -f.write(htmlcode) -f.write('

    ') -print '-'*79 - - -#=== LINKS =================================================================== - -# How to create a link: - -htmlcode = HTML.link('Decalage website', 'http://www.decalage.info') -print htmlcode -f.write(htmlcode) -f.write('

    ') -print '-'*79 - - -f.close() -print '\nOpen the file %s in a browser to see the result.' % HTMLFILE +# HTML.py tutorial - P. Lagadec + +# see also http://www.decalage.info/en/python/html for more details and +# updates. + + +import HTML + +# open an HTML file to show output in a browser +HTMLFILE = 'HTML_tutorial_output.html' +f = open(HTMLFILE, 'w') + + +#=== TABLES =================================================================== + +# 1) a simple HTML table may be built from a list of lists: + +table_data = [ + ['Last name', 'First name', 'Age'], + ['Smith', 'John', 30], + ['Carpenter', 'Jack', 47], + ['Johnson', 'Paul', 62], + ] + +htmlcode = HTML.table(table_data) +print(htmlcode) +f.write(htmlcode) +f.write('

    ') +print("-"*79) +#------------------------------------------------------------------------------- + +# 2) a header row may be specified: it will appear in bold in browsers + +table_data = [ + ['Smith', 'John', 30], + ['Carpenter', 'Jack', 47], + ['Johnson', 'Paul', 62], + ] + +htmlcode = HTML.table(table_data, + header_row=['Last name', 'First name', 'Age']) +print(htmlcode) +f.write(htmlcode) +f.write('

    ') +print("-"*79) +#------------------------------------------------------------------------------- + +# 3) you may also create a Table object and add rows one by one: + +t = HTML.Table(header_row=['x', 'square(x)', 'cube(x)']) +for x in range(1,10): + t.rows.append([x, x*x, x*x*x]) +htmlcode = str(t) +print(htmlcode) +f.write(htmlcode) +f.write('

    ') +print("-"*79) +#------------------------------------------------------------------------------- + +# 4) rows may be any iterable (list, tuple, ...) including a generator: +# (this is useful to save memory when generating a large table) + +def gen_rows(i): + 'rows generator' + for x in range(1,i): + yield [x, x*x, x*x*x] + +htmlcode = HTML.table(gen_rows(10), header_row=['x', 'square(x)', 'cube(x)']) +print(htmlcode) +f.write(htmlcode) +f.write('

    ') +print("-"*79) +#------------------------------------------------------------------------------- + +# 5) to choose a specific background color for a cell, use a TableCell +# object: + +HTML_COLORS = ['Black', 'Green', 'Silver', 'Lime', 'Gray', 'Olive', 'White', + 'Maroon', 'Navy', 'Red', 'Blue', 'Purple', 'Teal', 'Fuchsia', 'Aqua'] + +t = HTML.Table(header_row=['Name', 'Color']) +for colorname in HTML_COLORS: + colored_cell = HTML.TableCell(' ', bgcolor=colorname) + t.rows.append([colorname, colored_cell]) +htmlcode = str(t) +print(htmlcode) +f.write(htmlcode) +f.write('

    ') +print("-"*79) +#------------------------------------------------------------------------------- + +# 6) A simple way to generate a test report: + +# dictionary of test results, indexed by test id: +test_results = { + 'test 1': 'success', + 'test 2': 'failure', + 'test 3': 'success', + 'test 4': 'error', + } + +# dict of colors for each result: +result_colors = { + 'success': 'lime', + 'failure': 'red', + 'error': 'yellow', + } + +t = HTML.Table(header_row=['Test', 'Result']) +for test_id in sorted(test_results): + # create the colored cell: + color = result_colors[test_results[test_id]] + colored_result = HTML.TableCell(test_results[test_id], bgcolor=color) + # append the row with two cells: + t.rows.append([test_id, colored_result]) +htmlcode = str(t) +print(htmlcode) +f.write(htmlcode) +f.write('

    ') +print("-"*79) +#------------------------------------------------------------------------------- + +# 7) sample table with column attributes and styles: +table_data = [ + ['Smith', 'John', 30, 4.5], + ['Carpenter', 'Jack', 47, 7], + ['Johnson', 'Paul', 62, 10.55], + ] +htmlcode = HTML.table(table_data, + header_row = ['Last name', 'First name', 'Age', 'Score'], + col_width=['', '20%', '10%', '10%'], + col_align=['left', 'center', 'right', 'char'], + col_styles=['font-size: large', '', 'font-size: small', 'background-color:yellow']) +f.write(htmlcode + '

    \n') +print(htmlcode) +print("-"*79) +#=== LISTS =================================================================== + +# 1) a HTML list (with bullets) may be built from a Python list of strings: + +a_list = ['john', 'paul', 'jack'] +htmlcode = HTML.list(a_list) +print(htmlcode) +f.write(htmlcode) +f.write('

    ') +print("-"*79) +# 2) it is easy to change it into a numbered (ordered) list: + +htmlcode = HTML.list(a_list, ordered=True) +print(htmlcode) +f.write(htmlcode) +f.write('

    ') +print("-"*79) +# 3) Lines of a list may also be added one by one, when using the List class: + +html_list = HTML.List() +for i in range(1,10): + html_list.lines.append('square(%d) = %d' % (i, i*i)) +htmlcode = str(html_list) +print(htmlcode) +f.write(htmlcode) +f.write('

    ') +print("-"*79) +# 4) To save memory, a large list may be built from a generator: + +def gen_lines(i): + 'lines generator' + for x in range(1,i): + yield 'square(%d) = %d' % (x, x*x) +htmlcode = HTML.list(gen_lines(10)) +print(htmlcode) +f.write(htmlcode) +f.write('

    ') +print("-"*79) +#=== LINKS =================================================================== + +# How to create a link: + +htmlcode = HTML.link('Decalage website', 'http://www.decalage.info') +print(htmlcode) +f.write(htmlcode) +f.write('

    ') +print("-"*79) +f.close() +print('\nOpen the file %s in a browser to see the result.' % HTMLFILE) \ No newline at end of file diff --git a/thirdparty/HTML/__init__.py b/thirdparty/HTML/__init__.py index 9184ffc..fad1037 100644 --- a/thirdparty/HTML/__init__.py +++ b/thirdparty/HTML/__init__.py @@ -1 +1 @@ -from HTML import * \ No newline at end of file +from .HTML import * \ No newline at end of file diff --git a/thirdparty/HTMLParser_PL/HTMLParser_PL.py b/thirdparty/HTMLParser_PL/HTMLParser_PL.py index 8b471f8..b1e9d85 100644 --- a/thirdparty/HTMLParser_PL/HTMLParser_PL.py +++ b/thirdparty/HTMLParser_PL/HTMLParser_PL.py @@ -1,291 +1,299 @@ -#!/usr/bin/python -# -*- coding: iso-8859-1 -*- -""" -HTMLParser_PL - -Ce module contient la classe L{HTMLParser_PL.HTMLParser_PL} permettant -d'analyser un code HTML. Il s'agit d'une version amelioree du module -HTMLParser de la bibliotheque standard de Python 2.4 et 2.5, afin d'obtenir -un comportement plus proche d'Internet Explorer, notamment pour eviter certaines -techniques de camouflage de contenu malveillant. - -Projet: U{http://www.decalage.info/python/HTMLParser} - -@author: U{Philippe Lagadec} - -@contact: U{Philippe Lagadec} - -@license: PSF (Python Software Foundation) v2 - cf. code source ou http://www.python.org - -@version: 1.01 -@status: beta -""" - -__docformat__ = 'epytext en' - -#__author__ = "Philippe Lagadec" -__date__ = "2008-02-25" -__version__ = "1.01" - - -#------------------------------------------------------------------------------ -# LICENCE: - -# HTMLParser_PL is based on the HTMLParser source code from Python v2.5 - -# PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 -# -# 1. This LICENSE AGREEMENT is between the Python Software Foundation -# ("PSF"), and the Individual or Organization ("Licensee") accessing and -# otherwise using this software ("Python") in source or binary form and -# its associated documentation. -# -# 2. Subject to the terms and conditions of this License Agreement, PSF -# hereby grants Licensee a nonexclusive, royalty-free, world-wide -# license to reproduce, analyze, test, perform and/or display publicly, -# prepare derivative works, distribute, and otherwise use Python -# alone or in any derivative version, provided, however, that PSF's -# License Agreement and PSF's notice of copyright, i.e., "Copyright (c) -# 2001, 2002, 2003, 2004, 2005, 2006 Python Software Foundation; All Rights -# Reserved" are retained in Python alone or in any derivative version -# prepared by Licensee. -# -# 3. In the event Licensee prepares a derivative work that is based on -# or incorporates Python or any part thereof, and wants to make -# the derivative work available to others as provided herein, then -# Licensee hereby agrees to include in any such work a brief summary of -# the changes made to Python. -# -# 4. PSF is making Python available to Licensee on an "AS IS" -# basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR -# IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND -# DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS -# FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT -# INFRINGE ANY THIRD PARTY RIGHTS. -# -# 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON -# FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS -# A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, -# OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. -# -# 6. This License Agreement will automatically terminate upon a material -# breach of its terms and conditions. -# -# 7. Nothing in this License Agreement shall be deemed to create any -# relationship of agency, partnership, or joint venture between PSF and -# Licensee. This License Agreement does not grant permission to use PSF -# trademarks or trade name in a trademark sense to endorse or promote -# products or services of Licensee, or any third party. -# -# 8. By copying, installing or otherwise using Python, Licensee -# agrees to be bound by the terms and conditions of this License -# Agreement. - - -#------------------------------------------------------------------------------ -# HISTORIQUE: -# 01/11/2005 v0.01 PL: - 1ère version -# 04/11/2005 v0.02 PL: - ajout méthode feed() -# 12/01/2007 v1.00 PL: - version 1.00 officielle -# 2008-02-25 v1.01 PL: - ajout licence PSF -# - amelioration des regex pour etre plus robuste + tests - -#------------------------------------------------------------------------------ -# A FAIRE: -# ? passer en mode CDATA dans balises SCRIPT et STYLE ? (cf. specs HTML4) -# ? passer en mode CDATA dans attributs scripts/style ? -# - ajouter des options pour choisir le comportement du parseur -# - traduire en anglais -# - convertir les test en unittest -#------------------------------------------------------------------------------ - -#=== IMPORTS ================================================================== - -# modules standards Python: -import re, htmlentitydefs, sys, codecs -from HTMLParser import HTMLParser, tagfind, attrfind - - -#=== CONSTANTES =============================================================== - -# regex pour trouver des EntityRefs: -# (en théorie un nom d'entité pourrait contenir des tirets ou soulignés, mais -# il n'y en a pas dans la liste de Python) -# Par securite, on limite le nombre de caracteres a 32 dans le nom. -#entityref = re.compile('&([a-zA-Z][a-zA-Z0-9]*);?') -entityref = re.compile('&([a-zA-Z][a-zA-Z0-9]{0,31});?') - -# CharRef décimal: &# + 1 à 7 chiffres décimaux + point-virgule optionnel, -# suivi d'un caractère non-chiffre si pas de point-virgule et moins de 7 chiffres -#charref_dec = re.compile('&#([0-9]{1,7});?') -# En fait IE semble lire tous les chiffres fournis, mais génère une erreur -# quand ça dépasse 7 chiffres, donc on limite a 8 et on le verifie dans le code: -#charref_dec = re.compile('&#([0-9]+);?') -charref_dec = re.compile('&#([0-9]{1,8});?') -MAX_CHARREF_DEC = 7 - -# CharRef hexa: &#x ou &#X + 1 à 6 chiffres hexadécimaux + point-virgule optionnel -# (pas besoin de point-virgule si le caractère suivant n'est pas un chiffre hexa) -#charref_hex = re.compile('&#[xX]([0-9a-fA-F]{1,6});?') -# En fait IE semble lire tous les chiffres fournis, mais génère une erreur -# quand ça dépasse 6 chiffres hexa, donc on limite a 7 et on le verifie dans le -# code: -#charref_hex = re.compile('&#[xX]([0-9a-fA-F]+);?') -charref_hex = re.compile('&#[xX]([0-9a-fA-F]{1,7});?') -MAX_CHARREF_HEX = 6 - -#=== CLASSES ================================================================== - -#------------------------------------------------------------------------------ -# classe HTMLParser_PL -#----------------------- - -class HTMLParser_PL (HTMLParser): - """Version corrigée de la classe HTMLParser standard de Python v2.4.x.""" - - def feed(self, data): - """ - Pour transmettre des données à analyser par le parseur HTML. - Par rapport à la version standard de feed(), celle-ci filtre tous - les caractères nuls avant de parser les données, comme le fait - Internet Explorer. - """ - # on doit a priori distinguer les chaînes str et unicode: - if isinstance(data, str): - data = data.replace(chr(0), "") - elif isinstance(data, unicode): - data = data.replace(unichr(0), "") - else: - raise ValueError - # ensuite on appelle la méthode feed standard - HTMLParser.feed(self, data) - - def unescape(self, s): - """Pour décoder les CharRefs (&#NNN, &#xNNN) et EntityRefs (&, ...) - dans les attributs d'une balise HTML. - Remplace et corrige la méthode unescape() de HTMLParser. - """ - i = 0 - # on convertit la chaîne en unicode si ce n'est pas le cas - if not isinstance (s, unicode): - s = unicode(s, 'latin-1') - resultat = u"" - reste = s[i:] - #print "unescape('%s')" % s - while '&' in reste: - # on cherche le 1er "&": - j = reste.find('&') - resultat += reste[:j] - reste = reste[j:] - # on regarde d'abord si c'est un CharRef Hexa: - m = charref_hex.match(reste) - if m: - texte = m.group(1) - #print 'CharRef hexa: %s' % texte - # on vérifie qu'il y a au plus 6 chiffres hexa (limite d'IE) - if len(texte)>MAX_CHARREF_HEX: - # sinon on déclenche une exception: - self.error("CharRef with more than 6 hex digits") - # conversion hexa en entier - code = int(texte, 16) - # et on convertit en caractère unicode correspondant - resultat += unichr(code) - reste = reste[m.end():] - continue - # puis si c'est un CharRef Décimal: - m = charref_dec.match(reste) - if m: - texte = m.group(1) - #print 'CharRef decimal: %s' % texte - # on vérifie qu'il y a au plus 7 chiffres décimaux (limite d'IE) - if len(texte)>MAX_CHARREF_DEC: - # sinon on déclenche une exception: - self.error("CharRef with more than 7 decimal digits") - # conversion décimal en entier - code = int(texte) - # et on convertit en caractère unicode correspondant - resultat += unichr(code) - reste = reste[m.end():] - continue - # enfin si c'est un EntityRef: - m = entityref.match(reste) - if m and (m.group(1) in htmlentitydefs.name2codepoint): - #print 'EntityRef: %s' % m.group(1) - code = htmlentitydefs.name2codepoint[m.group(1)] - resultat += unichr(code) - reste = reste[m.end():] - continue - # sinon on avance simplement d'un caractère: - else: - resultat += '&' - reste = reste[1:] - return resultat + reste - - def parse_starttag(self, i): - """Remplace et corrige la méthode parse_starttag() de HTMLParser. - Ajoute à chaque attribut transmis à la méthode handle_starttag un - troisième item contenant la valeur d'origine de l'attribut, avant - conversion des CharRefs/EntityRefs. - """ - # les lignes modifiées par rapport à l'original sont marquées - # entre et - self.__starttag_text = None - endpos = self.check_for_whole_start_tag(i) - if endpos < 0: - return endpos - rawdata = self.rawdata - self.__starttag_text = rawdata[i:endpos] - - # Now parse the data between i+1 and j into a tag and attrs - attrs = [] - match = tagfind.match(rawdata, i+1) - assert match, 'unexpected call to parse_starttag()' - k = match.end() - self.lasttag = tag = rawdata[i+1:k].lower() - - while k < endpos: - m = attrfind.match(rawdata, k) - if not m: - break - attrname, rest, attrvalue = m.group(1, 2, 3) - if not rest: - attrvalue = None - elif attrvalue[:1] == '\'' == attrvalue[-1:] or \ - attrvalue[:1] == '"' == attrvalue[-1:]: - attrvalue = attrvalue[1:-1] - attrvalue = self.unescape(attrvalue) - # - # on doit convertir les CharRefs / EntityRefs même si la valeur de - # l'attribut n'est pas entre guillemets (comme IE): - else: - attrvalue = self.unescape(attrvalue) - # on ajoute à chaque attribut sa valeur non modifiée, pour une - # reconstruction la plus fidèle possible: - attr_raw = m.group() - attrs.append((attrname.lower(), attrvalue, attr_raw)) - #attrs.append((attrname.lower(), attrvalue)) - # - k = m.end() - - end = rawdata[k:endpos].strip() - if end not in (">", "/>"): - lineno, offset = self.getpos() - if "\n" in self.__starttag_text: - lineno = lineno + self.__starttag_text.count("\n") - offset = len(self.__starttag_text) \ - - self.__starttag_text.rfind("\n") - else: - offset = offset + len(self.__starttag_text) - self.error("junk characters in start tag: %r" - % (rawdata[k:endpos][:20],)) - if end.endswith('/>'): - # XHTML-style empty tag: - self.handle_startendtag(tag, attrs) - else: - self.handle_starttag(tag, attrs) - if tag in self.CDATA_CONTENT_ELEMENTS: - self.set_cdata_mode() - return endpos - - +#!/usr/bin/python +# -*- coding: iso-8859-1 -*- +""" +HTMLParser_PL + +Ce module contient la classe L{HTMLParser_PL.HTMLParser_PL} permettant +d'analyser un code HTML. Il s'agit d'une version amelioree du module +HTMLParser de la bibliotheque standard de Python 2.4 et 2.5, afin d'obtenir +un comportement plus proche d'Internet Explorer, notamment pour eviter certaines +techniques de camouflage de contenu malveillant. + +Projet: U{http://www.decalage.info/python/HTMLParser} + +@author: U{Philippe Lagadec} + +@contact: U{Philippe Lagadec} + +@license: PSF (Python Software Foundation) v2 + cf. code source ou http://www.python.org + +@version: 1.01 +@status: beta +""" + +__docformat__ = 'epytext en' + +#__author__ = "Philippe Lagadec" +__date__ = "2008-02-25" +__version__ = "1.01" + + +#------------------------------------------------------------------------------ +# LICENCE: + +# HTMLParser_PL is based on the HTMLParser source code from Python v2.5 + +# PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +# +# 1. This LICENSE AGREEMENT is between the Python Software Foundation +# ("PSF"), and the Individual or Organization ("Licensee") accessing and +# otherwise using this software ("Python") in source or binary form and +# its associated documentation. +# +# 2. Subject to the terms and conditions of this License Agreement, PSF +# hereby grants Licensee a nonexclusive, royalty-free, world-wide +# license to reproduce, analyze, test, perform and/or display publicly, +# prepare derivative works, distribute, and otherwise use Python +# alone or in any derivative version, provided, however, that PSF's +# License Agreement and PSF's notice of copyright, i.e., "Copyright (c) +# 2001, 2002, 2003, 2004, 2005, 2006 Python Software Foundation; All Rights +# Reserved" are retained in Python alone or in any derivative version +# prepared by Licensee. +# +# 3. In the event Licensee prepares a derivative work that is based on +# or incorporates Python or any part thereof, and wants to make +# the derivative work available to others as provided herein, then +# Licensee hereby agrees to include in any such work a brief summary of +# the changes made to Python. +# +# 4. PSF is making Python available to Licensee on an "AS IS" +# basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +# IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +# DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +# FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +# INFRINGE ANY THIRD PARTY RIGHTS. +# +# 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +# FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +# A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +# OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. +# +# 6. This License Agreement will automatically terminate upon a material +# breach of its terms and conditions. +# +# 7. Nothing in this License Agreement shall be deemed to create any +# relationship of agency, partnership, or joint venture between PSF and +# Licensee. This License Agreement does not grant permission to use PSF +# trademarks or trade name in a trademark sense to endorse or promote +# products or services of Licensee, or any third party. +# +# 8. By copying, installing or otherwise using Python, Licensee +# agrees to be bound by the terms and conditions of this License +# Agreement. + + +#------------------------------------------------------------------------------ +# HISTORIQUE: +# 01/11/2005 v0.01 PL: - 1ère version +# 04/11/2005 v0.02 PL: - ajout méthode feed() +# 12/01/2007 v1.00 PL: - version 1.00 officielle +# 2008-02-25 v1.01 PL: - ajout licence PSF +# - amelioration des regex pour etre plus robuste + tests + +#------------------------------------------------------------------------------ +# A FAIRE: +# ? passer en mode CDATA dans balises SCRIPT et STYLE ? (cf. specs HTML4) +# ? passer en mode CDATA dans attributs scripts/style ? +# - ajouter des options pour choisir le comportement du parseur +# - traduire en anglais +# - convertir les test en unittest +#------------------------------------------------------------------------------ + +#=== IMPORTS ================================================================== + +# modules standards Python: +import re, sys, codecs +try: + import html.entities as htmlentitydefs +except ImportError: + import htmlentitydefs +from html.parser import HTMLParser +try: + from html.parser import tagfind_tolerant as tagfind, attrfind_tolerant as attrfind +except ImportError: + from html.parser import tagfind, attrfind + + +#=== CONSTANTES =============================================================== + +# regex pour trouver des EntityRefs: +# (en théorie un nom d'entité pourrait contenir des tirets ou soulignés, mais +# il n'y en a pas dans la liste de Python) +# Par securite, on limite le nombre de caracteres a 32 dans le nom. +#entityref = re.compile('&([a-zA-Z][a-zA-Z0-9]*);?') +entityref = re.compile('&([a-zA-Z][a-zA-Z0-9]{0,31});?') + +# CharRef décimal: &# + 1 à 7 chiffres décimaux + point-virgule optionnel, +# suivi d'un caractère non-chiffre si pas de point-virgule et moins de 7 chiffres +#charref_dec = re.compile('&#([0-9]{1,7});?') +# En fait IE semble lire tous les chiffres fournis, mais génère une erreur +# quand ça dépasse 7 chiffres, donc on limite a 8 et on le verifie dans le code: +#charref_dec = re.compile('&#([0-9]+);?') +charref_dec = re.compile('&#([0-9]{1,8});?') +MAX_CHARREF_DEC = 7 + +# CharRef hexa: &#x ou &#X + 1 à 6 chiffres hexadécimaux + point-virgule optionnel +# (pas besoin de point-virgule si le caractère suivant n'est pas un chiffre hexa) +#charref_hex = re.compile('&#[xX]([0-9a-fA-F]{1,6});?') +# En fait IE semble lire tous les chiffres fournis, mais génère une erreur +# quand ça dépasse 6 chiffres hexa, donc on limite a 7 et on le verifie dans le +# code: +#charref_hex = re.compile('&#[xX]([0-9a-fA-F]+);?') +charref_hex = re.compile('&#[xX]([0-9a-fA-F]{1,7});?') +MAX_CHARREF_HEX = 6 + +#=== CLASSES ================================================================== + +#------------------------------------------------------------------------------ +# classe HTMLParser_PL +#----------------------- + +class HTMLParser_PL (HTMLParser): + """Version corrigée de la classe HTMLParser standard de Python v2.4.x.""" + + def feed(self, data): + """ + Pour transmettre des données à analyser par le parseur HTML. + Par rapport à la version standard de feed(), celle-ci filtre tous + les caractères nuls avant de parser les données, comme le fait + Internet Explorer. + """ + # on doit a priori distinguer les chaînes str et unicode: + if isinstance(data, str): + data = data.replace(chr(0), "") + elif isinstance(data, unicode): + data = data.replace(unichr(0), "") + else: + raise ValueError + # ensuite on appelle la méthode feed standard + HTMLParser.feed(self, data) + + def unescape(self, s): + """Pour décoder les CharRefs (&#NNN, &#xNNN) et EntityRefs (&, ...) + dans les attributs d'une balise HTML. + Remplace et corrige la méthode unescape() de HTMLParser. + """ + i = 0 + # on convertit la chaîne en unicode si ce n'est pas le cas + if not isinstance (s, unicode): + s = unicode(s, 'latin-1') + resultat = u"" + reste = s[i:] + #print "unescape('%s')" % s + while '&' in reste: + # on cherche le 1er "&": + j = reste.find('&') + resultat += reste[:j] + reste = reste[j:] + # on regarde d'abord si c'est un CharRef Hexa: + m = charref_hex.match(reste) + if m: + texte = m.group(1) + #print 'CharRef hexa: %s' % texte + # on vérifie qu'il y a au plus 6 chiffres hexa (limite d'IE) + if len(texte)>MAX_CHARREF_HEX: + # sinon on déclenche une exception: + self.error("CharRef with more than 6 hex digits") + # conversion hexa en entier + code = int(texte, 16) + # et on convertit en caractère unicode correspondant + resultat += unichr(code) + reste = reste[m.end():] + continue + # puis si c'est un CharRef Décimal: + m = charref_dec.match(reste) + if m: + texte = m.group(1) + #print 'CharRef decimal: %s' % texte + # on vérifie qu'il y a au plus 7 chiffres décimaux (limite d'IE) + if len(texte)>MAX_CHARREF_DEC: + # sinon on déclenche une exception: + self.error("CharRef with more than 7 decimal digits") + # conversion décimal en entier + code = int(texte) + # et on convertit en caractère unicode correspondant + resultat += unichr(code) + reste = reste[m.end():] + continue + # enfin si c'est un EntityRef: + m = entityref.match(reste) + if m and (m.group(1) in htmlentitydefs.name2codepoint): + #print 'EntityRef: %s' % m.group(1) + code = htmlentitydefs.name2codepoint[m.group(1)] + resultat += unichr(code) + reste = reste[m.end():] + continue + # sinon on avance simplement d'un caractère: + else: + resultat += '&' + reste = reste[1:] + return resultat + reste + + def parse_starttag(self, i): + """Remplace et corrige la méthode parse_starttag() de HTMLParser. + Ajoute à chaque attribut transmis à la méthode handle_starttag un + troisième item contenant la valeur d'origine de l'attribut, avant + conversion des CharRefs/EntityRefs. + """ + # les lignes modifiées par rapport à l'original sont marquées + # entre et + self.__starttag_text = None + endpos = self.check_for_whole_start_tag(i) + if endpos < 0: + return endpos + rawdata = self.rawdata + self.__starttag_text = rawdata[i:endpos] + + # Now parse the data between i+1 and j into a tag and attrs + attrs = [] + match = tagfind.match(rawdata, i+1) + assert match, 'unexpected call to parse_starttag()' + k = match.end() + self.lasttag = tag = rawdata[i+1:k].lower() + + while k < endpos: + m = attrfind.match(rawdata, k) + if not m: + break + attrname, rest, attrvalue = m.group(1, 2, 3) + if not rest: + attrvalue = None + elif attrvalue[:1] == '\'' == attrvalue[-1:] or \ + attrvalue[:1] == '"' == attrvalue[-1:]: + attrvalue = attrvalue[1:-1] + attrvalue = self.unescape(attrvalue) + # + # on doit convertir les CharRefs / EntityRefs même si la valeur de + # l'attribut n'est pas entre guillemets (comme IE): + else: + attrvalue = self.unescape(attrvalue) + # on ajoute à chaque attribut sa valeur non modifiée, pour une + # reconstruction la plus fidèle possible: + attr_raw = m.group() + attrs.append((attrname.lower(), attrvalue, attr_raw)) + #attrs.append((attrname.lower(), attrvalue)) + # + k = m.end() + + end = rawdata[k:endpos].strip() + if end not in (">", "/>"): + lineno, offset = self.getpos() + if "\n" in self.__starttag_text: + lineno = lineno + self.__starttag_text.count("\n") + offset = len(self.__starttag_text) \ + - self.__starttag_text.rfind("\n") + else: + offset = offset + len(self.__starttag_text) + self.error("junk characters in start tag: %r" + % (rawdata[k:endpos][:20],)) + if end.endswith('/>'): + # XHTML-style empty tag: + self.handle_startendtag(tag, attrs) + else: + self.handle_starttag(tag, attrs) + if tag in self.CDATA_CONTENT_ELEMENTS: + self.set_cdata_mode() + return endpos + + diff --git a/thirdparty/OleFileIO_PL/OleFileIO_PL.py b/thirdparty/OleFileIO_PL/OleFileIO_PL.py index c8052ae..60e0d61 100644 --- a/thirdparty/OleFileIO_PL/OleFileIO_PL.py +++ b/thirdparty/OleFileIO_PL/OleFileIO_PL.py @@ -192,7 +192,8 @@ #------------------------------------------------------------------------------ -import string, StringIO, struct, array, os.path, sys +import struct, array, os.path, sys +import io #[PL] Define explicitly the public API to avoid private objects in pydoc: __all__ = ['OleFileIO', 'isOleFile'] @@ -205,28 +206,18 @@ # on 64 bits platforms, integers in an array are 32 bits: UINT32 = 'I' else: - raise ValueError, 'Need to fix a bug with 32 bit arrays, please contact author...' + raise ValueError('Need to fix a bug with 32 bit arrays, please contact author...') #[PL] These workarounds were inspired from the Path module # (see http://www.jorendorff.com/articles/python/path/) #TODO: test with old Python versions -# Pre-2.3 workaround for booleans -try: - True, False -except NameError: - True, False = 1, 0 - -# Pre-2.3 workaround for basestring. +# basestring doesn't exist in Python 3 try: basestring except NameError: - try: - # is Unicode supported (Python >2.0 or >1.6 ?) - basestring = (str, unicode) - except NameError: - basestring = str + basestring = str #[PL] Experimental setting: if True, OLE filenames will be kept in Unicode # if False (default PIL behaviour), all filenames are converted to Latin-1. @@ -236,7 +227,7 @@ # command line to change it. DEBUG_MODE = False def debug_print(msg): - print msg + print(msg) def debug_pass(msg): pass debug = debug_pass @@ -254,18 +245,18 @@ def set_debug_mode(debug_mode): debug = debug_pass #TODO: convert this to hex -MAGIC = '\320\317\021\340\241\261\032\341' +MAGIC = b'\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1' #[PL]: added constants for Sector IDs (from AAF specifications) -MAXREGSECT = 0xFFFFFFFAL; # maximum SECT -DIFSECT = 0xFFFFFFFCL; # (-4) denotes a DIFAT sector in a FAT -FATSECT = 0xFFFFFFFDL; # (-3) denotes a FAT sector in a FAT -ENDOFCHAIN = 0xFFFFFFFEL; # (-2) end of a virtual stream chain -FREESECT = 0xFFFFFFFFL; # (-1) unallocated sector +MAXREGSECT = 0xFFFFFFFA; # maximum SECT +DIFSECT = 0xFFFFFFFC; # (-4) denotes a DIFAT sector in a FAT +FATSECT = 0xFFFFFFFD; # (-3) denotes a FAT sector in a FAT +ENDOFCHAIN = 0xFFFFFFFE; # (-2) end of a virtual stream chain +FREESECT = 0xFFFFFFFF; # (-1) unallocated sector #[PL]: added constants for Directory Entry IDs (from AAF specifications) -MAXREGSID = 0xFFFFFFFAL; # maximum directory entry ID -NOSTREAM = 0xFFFFFFFFL; # (-1) unallocated directory entry +MAXREGSID = 0xFFFFFFFA; # maximum directory entry ID +NOSTREAM = 0xFFFFFFFF; # (-1) unallocated directory entry #[PL] object types in storage (from AAF specifications) STGTY_EMPTY = 0 # empty directory entry (according to OpenOffice.org doc) @@ -293,7 +284,7 @@ def set_debug_mode(debug_mode): # map property id to name (for debugging purposes) VT = {} -for keyword, var in vars().items(): +for keyword, var in list(vars().items()): if keyword[:3] == "VT_": VT[var] = keyword @@ -313,7 +304,7 @@ def set_debug_mode(debug_mode): # impossible #[PL] add useful constants to __all__: -for key in vars().keys(): +for key in list(vars().keys()): if key.startswith('STGTY_') or key.startswith('DEFECT_'): __all__.append(key) @@ -335,25 +326,14 @@ def isOleFile (filename): #TODO: replace i16 and i32 with more readable struct.unpack equivalent -def i16(c, o = 0): - """ - Converts a 2-bytes (16 bits) string to an integer. - - c: string containing bytes to convert - o: offset of bytes to convert in string - """ - return ord(c[o])+(ord(c[o+1])<<8) +def i16(c, o=0): + """Converts a 2-bytes (16 bits) string/bytes to an unsigned integer.""" + return struct.unpack_from('256): - return filter(ord, s) - +def _unicode(s, errors='replace'): + """ + Decode a UTF-16LE encoded bytes object to a string. + s: UTF-16LE encoded bytes + errors: 'replace', 'ignore' or 'strict'. + """ + try: + return s.decode('UTF-16LE', errors) + except: + raise IOError('incorrect Unicode name') #=== CLASSES ================================================================== #--- _OleStream --------------------------------------------------------------- -class _OleStream(StringIO.StringIO): +class _OleStream(io.BytesIO): """ OLE2 Stream @@ -456,7 +406,7 @@ def __init__(self, fp, sect, size, offset, sectorsize, fat): debug(' sect=%d (%X), size=%d, offset=%d, sectorsize=%d, len(fat)=%d, fp=%s' %(sect,sect,size,offset,sectorsize,len(fat), repr(fp))) # for debugging messages, size of file where stream is read: - if isinstance(fp, StringIO.StringIO): + if isinstance(fp, io.BytesIO): filesize = len(fp.getvalue()) # file in MiniFAT else: filesize = os.path.getsize(fp.name) # file on disk @@ -471,12 +421,12 @@ def __init__(self, fp, sect, size, offset, sectorsize, fat): # and we keep a record that size was unknown: unknown_size = True debug(' stream with UNKNOWN SIZE') - nb_sectors = (size + (sectorsize-1)) / sectorsize + nb_sectors = (size + (sectorsize-1)) // sectorsize debug('nb_sectors = %d' % nb_sectors) # This number should (at least) be less than the total number of # sectors in the given FAT: if nb_sectors > len(fat): - raise IOError, 'malformed OLE document, stream too large' + raise IOError('malformed OLE document, stream too large') # optimization(?): data is first a list of strings, and join() is called # at the end to concatenate all in one string. # (this may not be really useful with recent Python versions) @@ -484,10 +434,10 @@ def __init__(self, fp, sect, size, offset, sectorsize, fat): # if size is zero, then first sector index should be ENDOFCHAIN: if size == 0 and sect != ENDOFCHAIN: debug('size == 0 and sect != ENDOFCHAIN:') - raise IOError, 'incorrect OLE sector index for empty stream' + raise IOError('incorrect OLE sector index for empty stream') #[PL] A fixed-length for loop is used instead of an undefined while # loop to avoid DoS attacks: - for i in xrange(nb_sectors): + for i in range(nb_sectors): # Sector index may be ENDOFCHAIN, but only if size was unknown if sect == ENDOFCHAIN: if unknown_size: @@ -495,7 +445,7 @@ def __init__(self, fp, sect, size, offset, sectorsize, fat): else: # else this means that the stream is smaller than declared: debug('sect=ENDOFCHAIN before expected size') - raise IOError, 'incomplete OLE stream' + raise IOError('incomplete OLE stream') # sector index should be within FAT: if sect<0 or sect>=len(fat): debug('sect=%d (%X) / len(fat)=%d' % (sect, sect, len(fat))) @@ -505,7 +455,7 @@ def __init__(self, fp, sect, size, offset, sectorsize, fat): ## f.write(tmp_data) ## f.close() ## debug('data read so far: %d bytes' % len(tmp_data)) - raise IOError, 'incorrect OLE FAT, sector index out of range' + raise IOError('incorrect OLE FAT, sector index out of range') #TODO: merge this code with OleFileIO.getsect() ? #TODO: check if this works with 4K sectors: try: @@ -513,7 +463,7 @@ def __init__(self, fp, sect, size, offset, sectorsize, fat): except: debug('sect=%d, seek=%d, filesize=%d' % (sect, offset+sectorsize*sect, filesize)) - raise IOError, 'OLE sector index out of range' + raise IOError('OLE sector index out of range') sector_data = fp.read(sectorsize) # [PL] check if there was enough data: # Note: if sector is the last of the file, sometimes it is not a @@ -523,18 +473,18 @@ def __init__(self, fp, sect, size, offset, sectorsize, fat): debug('sect=%d / len(fat)=%d, seek=%d / filesize=%d, len read=%d' % (sect, len(fat), offset+sectorsize*sect, filesize, len(sector_data))) debug('seek+len(read)=%d' % (offset+sectorsize*sect+len(sector_data))) - raise IOError, 'incomplete OLE sector' + raise IOError('incomplete OLE sector') data.append(sector_data) # jump to next sector in the FAT: try: sect = fat[sect] except IndexError: # [PL] if pointer is out of the FAT an exception is raised - raise IOError, 'incorrect OLE FAT, sector index out of range' + raise IOError('incorrect OLE FAT, sector index out of range') #[PL] Last sector should be a "end of chain" marker: if sect != ENDOFCHAIN: - raise IOError, 'incorrect last sector index in OLE stream' - data = string.join(data, "") + raise IOError('incorrect last sector index in OLE stream') + data = b"".join(data) # Data is truncated to the actual stream size: if len(data) >= size: data = data[:size] @@ -547,9 +497,9 @@ def __init__(self, fp, sect, size, offset, sectorsize, fat): else: # read data is less than expected: debug('len(data)=%d, size=%d' % (len(data), size)) - raise IOError, 'OLE stream size is less than declared' + raise IOError('OLE stream size is less than declared') # when all data is read in memory, StringIO constructor is called - StringIO.StringIO.__init__(self, data) + io.BytesIO.__init__(self, data) # Then the _OleStream object can be used as a read-only file object. @@ -653,13 +603,13 @@ def __init__(self, entry, sid, olefile): # sectors, BUT apparently some implementations set it as 0xFFFFFFFFL, 1 # or some other value so it cannot be raised as a defect in general: if olefile.sectorsize == 512: - if sizeHigh != 0 and sizeHigh != 0xFFFFFFFFL: + if sizeHigh != 0 and sizeHigh != 0xFFFFFFFF: debug('sectorsize=%d, sizeLow=%d, sizeHigh=%d (%X)' % (olefile.sectorsize, sizeLow, sizeHigh, sizeHigh)) olefile._raise_defect(DEFECT_UNSURE, 'incorrect OLE stream size') self.size = sizeLow else: - self.size = sizeLow + (long(sizeHigh)<<32) + self.size = sizeLow + (int(sizeHigh)<<32) debug(' - size: %d (sizeLow=%d, sizeHigh=%d)' % (self.size, sizeLow, sizeHigh)) self.clsid = _clsid(clsid) @@ -729,7 +679,7 @@ def append_kids(self, child_sid): self.append_kids(child.sid_left) # Check if its name is not already used (case-insensitive): name_lower = child.name.lower() - if self.kids_dict.has_key(name_lower): + if name_lower in self.kids_dict: self.olefile._raise_defect(DEFECT_INCORRECT, "Duplicate filename in OLE storage") # Then the child_sid _OleDirectoryEntry object is appended to the @@ -747,9 +697,9 @@ def append_kids(self, child_sid): child.build_storage_tree() - def __cmp__(self, other): + def __lt__(self, other): "Compare entries by name" - return cmp(self.name, other.name) + return self.name < other.name #TODO: replace by the same function as MS implementation ? # (order by name length first, then case-insensitive order) @@ -758,12 +708,12 @@ def dump(self, tab = 0): "Dump this entry, and all its subentries (for debug purposes only)" TYPES = ["(invalid)", "(storage)", "(stream)", "(lockbytes)", "(property)", "(root)"] - print " "*tab + repr(self.name), TYPES[self.entry_type], + print(" "*tab + repr(self.name), TYPES[self.entry_type], end=' ') if self.entry_type in (STGTY_STREAM, STGTY_ROOT): - print self.size, "bytes", - print + print(self.size, "bytes", end=' ') + print() if self.entry_type in (STGTY_STORAGE, STGTY_ROOT) and self.clsid: - print " "*tab + "{%s}" % self.clsid + print(" "*tab + "{%s}" % self.clsid) for kid in self.kids: kid.dump(tab + 2) @@ -829,7 +779,7 @@ def _raise_defect(self, defect_level, message): """ # added by [PL] if defect_level >= self._raise_defects_level: - raise IOError, message + raise IOError(message) def open(self, filename): @@ -925,10 +875,10 @@ def open(self, filename): ) = struct.unpack(fmt_header, header1) debug( struct.unpack(fmt_header, header1)) - if self.Sig != '\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1': + if self.Sig != b'\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1': # OLE signature should always be present self._raise_defect(DEFECT_FATAL, "incorrect OLE signature") - if self.clsid != '\x00'*16: + if self.clsid != b'\x00'*16: # according to AAF specs, CLSID should always be zero self._raise_defect(DEFECT_INCORRECT, "incorrect CLSID in OLE header") debug( "MinorVersion = %d" % self.MinorVersion ) @@ -974,7 +924,7 @@ def open(self, filename): # calculate the number of sectors in the file # (-1 because header doesn't count) filesize = os.path.getsize(filename) - self.nb_sect = ( (filesize + self.SectorSize-1) / self.SectorSize) - 1 + self.nb_sect = ( (filesize + self.SectorSize-1) // self.SectorSize) - 1 debug( "Number of sectors in the file: %d" % self.nb_sect ) # file clsid (probably never used, so we don't store it) @@ -1041,14 +991,14 @@ def dumpfat(self, fat, firstindex=0): DIFSECT: "DIFSECT " } nbsect = len(fat) - nlines = (nbsect+VPL-1)/VPL - print "index", + nlines = (nbsect+VPL-1)//VPL + print("index", end=' ') for i in range(VPL): - print ("%8X" % i), - print "" + print(("%8X" % i), end=' ') + print() for l in range(nlines): index = l*VPL - print ("%8X:" % (firstindex+index)), + print(("%8X:" % (firstindex+index)), end=' ') for i in range(index, index+VPL): if i>=nbsect: break @@ -1060,8 +1010,8 @@ def dumpfat(self, fat, firstindex=0): nom = " --->" else: nom = "%8X" % sect - print nom, - print "" + print(nom, end=' ') + print() def dumpsect(self, sector, firstindex=0): @@ -1071,21 +1021,21 @@ def dumpsect(self, sector, firstindex=0): VPL=8 # number of values per line (8+1 * 8+1 = 81) tab = array.array(UINT32, sector) nbsect = len(tab) - nlines = (nbsect+VPL-1)/VPL - print "index", + nlines = (nbsect+VPL-1)//VPL + print("index", end=' ') for i in range(VPL): - print ("%8X" % i), - print "" + print(("%8X" % i), end=' ') + print() for l in range(nlines): index = l*VPL - print ("%8X:" % (firstindex+index)), + print(("%8X:" % (firstindex+index)), end=' ') for i in range(index, index+VPL): if i>=nbsect: break sect = tab[i] nom = "%8X" % sect - print nom, - print "" + print(nom, end=' ') + print() def sect2array(self, sect): """ @@ -1166,12 +1116,12 @@ def loadfat(self, header): debug( "DIFAT analysis..." ) # We compute the necessary number of DIFAT sectors : # (each DIFAT sector = 127 pointers + 1 towards next DIFAT sector) - nb_difat = (self.csectFat-109 + 126)/127 + nb_difat = (self.csectFat-109 + 126)//127 debug( "nb_difat = %d" % nb_difat ) if self.csectDif != nb_difat: - raise IOError, 'incorrect DIFAT' + raise IOError('incorrect DIFAT') isect_difat = self.sectDifStart - for i in xrange(nb_difat): + for i in range(nb_difat): debug( "DIFAT block %d, sector %X" % (i, isect_difat) ) #TODO: check if corresponding FAT SID = DIFSECT sector_difat = self.getsect(isect_difat) @@ -1184,11 +1134,11 @@ def loadfat(self, header): # checks: if isect_difat not in [ENDOFCHAIN, FREESECT]: # last DIFAT pointer value must be ENDOFCHAIN or FREESECT - raise IOError, 'incorrect end of DIFAT' + raise IOError('incorrect end of DIFAT') ## if len(self.fat) != self.csectFat: ## # FAT should contain csectFat blocks -## print "FAT length: %d instead of %d" % (len(self.fat), self.csectFat) -## raise IOError, 'incorrect DIFAT' +## print("FAT length: %d instead of %d" % (len(self.fat), self.csectFat)) +## raise IOError('incorrect DIFAT') # since FAT is read from fixed-size sectors, it may contain more values # than the actual number of sectors in the file. # Keep only the relevant sector indexes: @@ -1214,7 +1164,7 @@ def loadminifat(self): # 2) Actually used size is calculated by dividing the MiniStream size # (given by root entry size) by the size of mini sectors, *4 for # 32 bits indexes: - nb_minisectors = (self.root.size + self.MiniSectorSize-1) / self.MiniSectorSize + nb_minisectors = (self.root.size + self.MiniSectorSize-1) // self.MiniSectorSize used_size = nb_minisectors * 4 debug('loadminifat(): minifatsect=%d, nb FAT sectors=%d, used_size=%d, stream_size=%d, nb MiniSectors=%d' % (self.minifatsect, self.csectMiniFat, used_size, stream_size, nb_minisectors)) @@ -1272,7 +1222,7 @@ def loaddirectory(self, sect): #[PL] to detect malformed documents and avoid DoS attacks, the maximum # number of directory entries can be calculated: - max_entries = self.directory_fp.size / 128 + max_entries = self.directory_fp.size // 128 debug('loaddirectory: size=%d, max_entries=%d' % (self.directory_fp.size, max_entries)) @@ -1406,7 +1356,7 @@ def _find(self, filename): if kid.name.lower() == name.lower(): break else: - raise IOError, "file not found" + raise IOError("file not found") node = kid return node.sid @@ -1426,7 +1376,7 @@ def openstream(self, filename): sid = self._find(filename) entry = self.direntries[sid] if entry.entry_type != STGTY_STREAM: - raise IOError, "this file is not a stream" + raise IOError("this file is not a stream") return self._open(entry.isectStart, entry.size) @@ -1476,7 +1426,7 @@ def get_size(self, filename): entry = self.direntries[sid] if entry.entry_type != STGTY_STREAM: #TODO: Should it return zero instead of raising an exception ? - raise TypeError, 'object is not an OLE stream' + raise TypeError('object is not an OLE stream') return entry.size @@ -1509,7 +1459,7 @@ def getproperties(self, filename): fp.seek(i32(s, 16)) # get section - s = "****" + fp.read(i32(fp.read(4))-4) + s = b"****" + fp.read(i32(fp.read(4))-4) for i in range(i32(s, 4)): @@ -1542,12 +1492,12 @@ def getproperties(self, filename): count = i32(s, offset+4) value = self._unicode(s[offset+8:offset+8+count*2]) elif type == VT_FILETIME: - value = long(i32(s, offset+4)) + (long(i32(s, offset+8))<<32) + value = i32(s, offset+4) + (i32(s, offset+8)<<32) # FIXME: this is a 64-bit int: "number of 100ns periods # since Jan 1,1601". Should map this to Python time - value = value / 10000000L # seconds + value = value // 10000000 # seconds elif type == VT_UI1: - value = ord(s[offset+4]) + value = s[offset+4] if isinstance(s[offset+4], int) else ord(s[offset+4]) elif type == VT_CLSID: value = _clsid(s[offset+4:offset+20]) elif type == VT_CF: @@ -1576,8 +1526,8 @@ def getproperties(self, filename): # [PL] display quick usage info if launched from command-line if len(sys.argv) <= 1: - print __doc__ - print """ + print(__doc__) + print(""" Launched from command line, this script parses OLE files and prints info. Usage: OleFileIO_PL.py [-d] [-c] [file2 ...] @@ -1585,7 +1535,7 @@ def getproperties(self, filename): Options: -d : debug mode (display a lot of debug information, for developers only) -c : check all streams (for debugging purposes) -""" +""") sys.exit() check_streams = False @@ -1602,16 +1552,15 @@ def getproperties(self, filename): continue ole = OleFileIO(filename, raise_defects=DEFECT_INCORRECT) - print "-" * 68 - print filename - print "-" * 68 + print("-" * 68) + print(filename) + print("-" * 68) ole.dumpdirectory() for streamname in ole.listdir(): if streamname[-1][0] == "\005": - print streamname, ": properties" + print(streamname, ": properties") props = ole.getproperties(streamname) - props = props.items() - props.sort() + props = sorted(props.items()) for k, v in props: #[PL]: avoid to display too large or binary values: if isinstance(v, basestring): @@ -1623,31 +1572,31 @@ def getproperties(self, filename): if chr(c) in v: v = '(binary data)' break - print " ", k, v + print(" ", k, v) if check_streams: # Read all streams to check if there are errors: - print '\nChecking streams...' + print('\nChecking streams...') for streamname in ole.listdir(): # print name using repr() to convert binary chars to \xNN: - print '-', repr('/'.join(streamname)),'-', + print('-', repr('/'.join(streamname)),'-', end=' ') st_type = ole.get_type(streamname) if st_type == STGTY_STREAM: - print 'size %d' % ole.get_size(streamname) + print('size %d' % ole.get_size(streamname)) # just try to read stream in memory: ole.openstream(streamname) else: - print 'NOT a stream : type=%d' % st_type - print '' + print('NOT a stream : type=%d' % st_type) + print() #[PL] Test a few new methods: root = ole.get_rootentry_name() - print 'Root entry name: "%s"' % root + print('Root entry name: "%s"' % root) if ole.exists('worddocument'): - print "This is a Word document." - print "type of stream 'WordDocument':", ole.get_type('worddocument') - print "size :", ole.get_size('worddocument') + print("This is a Word document.") + print("type of stream 'WordDocument':", ole.get_type('worddocument')) + print("size :", ole.get_size('worddocument')) if ole.exists('macros/vba'): - print "This document may contain VBA macros." + print("This document may contain VBA macros.") ## except IOError, v: -## print "***", "cannot read", file, "-", v +## print("***", "cannot read", file, "-", v) diff --git a/thirdparty/OleFileIO_PL/setup.py b/thirdparty/OleFileIO_PL/setup.py index 739c28f..012a1e3 100644 --- a/thirdparty/OleFileIO_PL/setup.py +++ b/thirdparty/OleFileIO_PL/setup.py @@ -1,43 +1,43 @@ -# Setup script for OleFileIO_PL - Philippe Lagadec - -# History: -# 2007-09-13 v0.01 PL: - first version -# 2007-11-10 v0.02 PL: - updated website URL -# 2007-12-04 v0.03 PL: - updated description, added debug_mode test -# 2009-09-11 v0.04 PL: - updated URL, e-mail, licence, disabled e-mail - -import distutils.core - -from OleFileIO_PL import __version__, __author__, DEBUG_MODE - -# debug mode should be off for usual releases: -if DEBUG_MODE: - raise ValueError, "WARNING: DEBUG_MODE should be False !" - -kw = { - 'name': "OleFileIO_PL", - 'version': __version__, - 'description': "A Python module to parse and read Microsoft OLE2 files (Structured Storage or Compound Document, Microsoft Office) - Improved version of the OleFileIO module from PIL, the Python Image Library.", - 'author': __author__, - #'author_email': "decalage(a)laposte.net", - 'url': "http://www.decalage.info/python/olefileio", - 'license': "updated PIL license (see source code or LICENCE.txt)", - 'py_modules': ['OleFileIO_PL'], - } - - -# If we're running Python 2.3, add extra information -if hasattr(distutils.core, 'setup_keywords'): - if 'classifiers' in distutils.core.setup_keywords: - kw['classifiers'] = [ - 'Development Status :: 4 - Beta', - 'License :: OSI Approved', #'License :: PIL license (see source code)', - 'Intended Audience :: Developers', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Topic :: Software Development :: Libraries :: Python Modules' - ] - if 'download_url' in distutils.core.setup_keywords: - kw['download_url'] = "http://www.decalage.info/python/olefileio" - -distutils.core.setup(**kw) +# Setup script for OleFileIO_PL - Philippe Lagadec + +# History: +# 2007-09-13 v0.01 PL: - first version +# 2007-11-10 v0.02 PL: - updated website URL +# 2007-12-04 v0.03 PL: - updated description, added debug_mode test +# 2009-09-11 v0.04 PL: - updated URL, e-mail, licence, disabled e-mail + +import distutils.core + +from OleFileIO_PL import __version__, __author__, DEBUG_MODE + +# debug mode should be off for usual releases: +if DEBUG_MODE: + raise ValueError("WARNING: DEBUG_MODE should be False !") + +kw = { + 'name': "OleFileIO_PL", + 'version': __version__, + 'description': "A Python module to parse and read Microsoft OLE2 files (Structured Storage or Compound Document, Microsoft Office) - Improved version of the OleFileIO module from PIL, the Python Image Library.", + 'author': __author__, + #'author_email': "decalage(a)laposte.net", + 'url': "http://www.decalage.info/python/olefileio", + 'license': "updated PIL license (see source code or LICENCE.txt)", + 'py_modules': ['OleFileIO_PL'], + } + + +# If we're running Python 2.3, add extra information +if hasattr(distutils.core, 'setup_keywords'): + if 'classifiers' in distutils.core.setup_keywords: + kw['classifiers'] = [ + 'Development Status :: 4 - Beta', + 'License :: OSI Approved', #'License :: PIL license (see source code)', + 'Intended Audience :: Developers', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Topic :: Software Development :: Libraries :: Python Modules' + ] + if 'download_url' in distutils.core.setup_keywords: + kw['download_url'] = "http://www.decalage.info/python/olefileio" + +distutils.core.setup(**kw) diff --git a/thirdparty/RechercherRemplacer/RechercherRemplacer.py b/thirdparty/RechercherRemplacer/RechercherRemplacer.py index 98c69a2..550bbc7 100644 --- a/thirdparty/RechercherRemplacer/RechercherRemplacer.py +++ b/thirdparty/RechercherRemplacer/RechercherRemplacer.py @@ -172,6 +172,12 @@ def __init__(self, regex, case_sensitive=True, remplacement=False): else: flags = re.IGNORECASE # on compile la regex pour de meilleures perfos + # In Python 3, if regex is bytes, remplacement must also be bytes + if isinstance(regex, bytes) and isinstance(remplacement, str): + remplacement = remplacement.encode('latin-1') + self.remplacement = remplacement + elif isinstance(regex, str) and isinstance(remplacement, bytes): + regex = regex.encode('latin-1') self.regex = re.compile(regex, flags) def rechercher_remplacer(self, chaine, controle_apres=True, taille_identique=True): @@ -198,83 +204,83 @@ def rechercher_remplacer(self, chaine, controle_apres=True, taille_identique=Tru # cf. aide re: retourne (new_string, number_of_subs_made). if taille_identique: if len(chaine) != len(resultat): - raise RuntimeError, "taille modifiee apres remplacement" + raise RuntimeError("taille modifiee apres remplacement") if controle_apres: if self.regex.search(resultat): - raise RuntimeError, "motif toujours present apres remplacement" + raise RuntimeError("motif toujours present apres remplacement") return resultat, nb_rempl #=== PROGRAMME PRINCIPAL (auto-test) ========================================== if __name__ == "__main__": - print "--------------------------------------" - print "TEST DU MODULE RechercherRemplacer.py:" - print "--------------------------------------" - print "" - print "1) test de la classe Motif" - print "" + print("--------------------------------------") + print("TEST DU MODULE RechercherRemplacer.py:") + print("--------------------------------------") + print("") + print("1) test de la classe Motif") + print("") c = '...Toto...tOTO...Titi...' - print "chaine: '%s'" % c - print "- recherche de 'Toto' sans remplacement, case sensitive:" + print("chaine: '%s'" % c) + print("- recherche de 'Toto' sans remplacement, case sensitive:") m = Motif(r"Toto") - print m.rechercher_remplacer(c) - print "- recherche de 'toto' sans remplacement, case INsensitive:" + print(m.rechercher_remplacer(c)) + print("- recherche de 'toto' sans remplacement, case INsensitive:") mi = Motif(r"toto", case_sensitive=False) - print mi.rechercher_remplacer(c) - print "- recherche de 'Toto' AVEC remplacement, case sensitive:" + print(mi.rechercher_remplacer(c)) + print("- recherche de 'Toto' AVEC remplacement, case sensitive:") mr = Motif(r"Toto", remplacement=r"Tata") - print mr.rechercher_remplacer(c) - print "- recherche de 'toto' AVEC remplacement, case INsensitive:" + print(mr.rechercher_remplacer(c)) + print("- recherche de 'toto' AVEC remplacement, case INsensitive:") mri = Motif(r"toto", remplacement=r"tAtA", case_sensitive=False) - print mri.rechercher_remplacer(c) + print(mri.rechercher_remplacer(c)) - print "" - print "Memes tests avec une chaine Unicode:" + print("") + print("Memes tests avec une chaine Unicode:") c = u'...Toto...tOTO...Titi...' - print "chaine: '%s'" % c - print "- recherche de 'Toto' sans remplacement, case sensitive:" + print("chaine: '%s'" % c) + print("- recherche de 'Toto' sans remplacement, case sensitive:") m = Motif(r"Toto") - print m.rechercher_remplacer(c) - print "- recherche de 'toto' sans remplacement, case INsensitive:" + print(m.rechercher_remplacer(c)) + print("- recherche de 'toto' sans remplacement, case INsensitive:") mi = Motif(r"toto", case_sensitive=False) - print mi.rechercher_remplacer(c) - print "- recherche de 'Toto' AVEC remplacement, case sensitive:" + print(mi.rechercher_remplacer(c)) + print("- recherche de 'Toto' AVEC remplacement, case sensitive:") mr = Motif(r"Toto", remplacement=r"Tata") - print mr.rechercher_remplacer(c) - print "- recherche de 'toto' AVEC remplacement, case INsensitive:" + print(mr.rechercher_remplacer(c)) + print("- recherche de 'toto' AVEC remplacement, case INsensitive:") mri = Motif(r"toto", remplacement=r"tAtA", case_sensitive=False) - print mri.rechercher_remplacer(c) + print(mri.rechercher_remplacer(c)) - print "" - print "test si une exception est bien levee en cas de controle apres remplacement positif:" + print("") + print("test si une exception est bien levee en cas de controle apres remplacement positif:") c = "abcdcd" motif = "abcd" rempl = "ab" - print "chaine: '%s'" % c - print "motif: '%s' remplace par '%s'" % (motif, rempl) + print("chaine: '%s'" % c) + print("motif: '%s' remplace par '%s'" % (motif, rempl)) m = Motif(motif, remplacement=rempl) try: # on met taille_identique=False pour éviter de lever cette exception - print m.rechercher_remplacer(c, taille_identique=False) - print "NOK, pas d'exception !" + print(m.rechercher_remplacer(c, taille_identique=False)) + print("NOK, pas d'exception !") except: - print "OK, exception levee." + print("OK, exception levee.") - print "" - print "test si une exception est bien levee en cas de taille differente apres remplacement:" + print("") + print("test si une exception est bien levee en cas de taille differente apres remplacement:") try: # on met controle_apres=False pour éviter de lever cette exception - print m.rechercher_remplacer(c, controle_apres=False) - print "NOK, pas d'exception !" + print(m.rechercher_remplacer(c, controle_apres=False)) + print("NOK, pas d'exception !") except: - print "OK, exception levee." + print("OK, exception levee.") - print "----------------------------------------" - print "" - print "2) test de la fonction rechercherRemplacer" - print "" - print "Creation d'un fichier court" + print("----------------------------------------") + print("") + print("2) test de la fonction rechercherRemplacer") + print("") + print("Creation d'un fichier court") f = file("fich1_src.txt", "w") f.write('...abc...DEF...ABCdef...') f.close() @@ -283,11 +289,11 @@ def rechercher_remplacer(self, chaine, controle_apres=True, taille_identique=Tru m1 = Motif(r'abc', remplacement=r'xyz') m2 = Motif(r'def', remplacement=r'<<<', case_sensitive=False) n = rechercherRemplacer([m1, m2], fs, fd) - print "%d remplacements." % n + print("%d remplacements." % n) fs.close() fd.close() - print "Creation d'un fichier long, avec caracteres nuls" + print("Creation d'un fichier long, avec caracteres nuls") f = file("fich2_src.txt", "w") ligne = "." * 80 + "\n" motif1 = r"test\000" @@ -300,7 +306,7 @@ def rechercher_remplacer(self, chaine, controle_apres=True, taille_identique=Tru m1 = Motif(motif1, remplacement=r"TOTO\000", case_sensitive=False) m2 = Motif(motif2, remplacement=r"\000toto", case_sensitive=False) n = rechercherRemplacer([m1, m2], fs, fd) - print "%d remplacements." % n + print("%d remplacements." % n) fs.close() fd.close() diff --git a/thirdparty/cherryproxy/CherryProxy.py b/thirdparty/cherryproxy/CherryProxy.py index 5d28a61..f90d634 100644 --- a/thirdparty/cherryproxy/CherryProxy.py +++ b/thirdparty/cherryproxy/CherryProxy.py @@ -1,658 +1,658 @@ -""" -CherryProxy - -a lightweight HTTP proxy based on the CherryPy WSGI server and httplib, -extensible for content analysis and filtering. - -AUTHOR: Philippe Lagadec (decalage at laposte dot net) - -PROJECT WEBSITE: http://www.decalage.info/python/cherryproxy - -LICENSE: - -Copyright (c) 2008-2011, Philippe Lagadec (decalage at laposte dot net) - -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above copyright -notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND -FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR -OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -PERFORMANCE OF THIS SOFTWARE. - -Usage: -- either run this script directly for a demo, and use localhost:8070 as proxy. -- or create a class inheriting from CherryProxy and implement the methods - filter_request and filter_response as desired. See the example scripts for - more information. - -Usage as a script: CherryProxy.py [options] - -Options: - -h, --help show this help message and exit - -p PORT, --port=PORT port for HTTP proxy, 8070 by default - -a ADDRESS, --address=ADDRESS - IP address of interface for HTTP proxy (0.0.0.0 for - all, default=localhost) - -f PROXY, --forward=PROXY - Forward requests to parent proxy, specified as - hostname[:port] or IP address[:port] - -v, --verbose Verbose mode, display debugging messages - -l, --log Log each request to a separate file (use with -v) -""" - -#------------------------------------------------------------------------------ -# CHANGELOG: -# 2008-11-01 v0.01 PL: - first version -# 2008-11-02 v0.02 PL: - extensible CherryProxy class instead of functions -# 2009-05-05 v0.03 PL: - added comments and license -# - option to set server name in constructor -# 2009-05-06 v0.04 PL: - forward request body to server -# 2010-04-25 v0.05 PL: - moved nozip demo to separate script -# - debug option to enable/disable debug output -# 2011-09-03 v0.06 PL: - replaced attributes by thread local variables to -# support multithreading -# 2011-09-07 v0.07 PL: - use logging instead of print for debugging -# - split proxy_app into several methods -# - close each http connection to server -# 2011-09-15 v0.08 PL: - command-line options -# 2011-09-21 v0.09 PL: - separate main function (to be used in examples) -# 2011-09-24 v0.10 PL: - renamed adapt to filter -# - added methods to send response without forwarding -# request to server -# 2011-09-30 v0.11 PL: - added methods to filter headers before reading body -# 2011-11-15 v0.12 PL: - moved and renamed private methods with an underscore -# - added option to use a parent proxy -# 2011-11-29 v0.13 PL: - new option (-l) to log each request to a file -# - black list to block unsupported HTTP methods and schemes - -#------------------------------------------------------------------------------ -# TODO: -# + log_file: fix debug level without -v, add formatter -# + CLI option to dump request and response data using repr() -# + disable debug options -# + fix examples, using CT+filename, blocking some requests -# + simple doc describing API -# + methods to parse useful headers: content-type, content-disposition, etc -# http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.17 -# http://www.ietf.org/rfc/rfc2183.txt -# + method to disable logging (if log_level=None) and to add a dummy handler -# + init option to enable debug messages or not -# + force connection close and remove keep-alive on server side -# + close connection on server side when needed -# + _send_request: reconstruct URL from its elements (if they were changed) -# + _send_request: handle connection errors - -# TODO LATER: -# - update CherryPy WSGI server to the latest version 3.2.2 -# - option to save each request and response (before and after adaptation) to a file -# - later, reuse http connection when no connection close header or keep-alive -# - option to uncompress body when gzip/deflate/compress is used -# - support HTTPS -# ? config file to set options? -# ? use urllib2 instead of httplib? - -#--- IMPORTS ------------------------------------------------------------------ - -from cherrypy import wsgiserver -import urlparse, urllib2, httplib, sys, threading, logging - - -#--- CONSTANTS ---------------------------------------------------------------- - -__version__ = '0.13' - -SERVER_NAME = 'CherryProxy/%s' % __version__ - -# Not supported methods and schemes -BLACKLIST_METHODS = ['CONNECT'] -BLACKLIST_SCHEMES = ['https'] - - -#=== CLASSES ================================================================== - -class CherryProxy (object): - """ - CherryProxy: class implementing a filtering HTTP proxy - - To use it, create a class inheriting from CherryProxy and implement the - methods filter_request and filter_response as desired. - Then call the start method to start the proxy. - Note: the logging module needs to be initialized before creating a - CherryProxy object. - See the example scripts for more information. - """ - - # class variables: - # unique id for each request - _reqid = 0 - _lock_reqid = threading.Lock() - - def __init__(self, address='localhost', port=8070, server_name=SERVER_NAME, - debug=False, log_level=logging.INFO, options=None, parent_proxy=None, - log_file=False): - """ - CherryProxy constructor - - address: IP address of interface to listen to, or 0.0.0.0 for all - (localhost by default) - port: TCP port for the proxy (8070 by default) - server_name: server name used in HTTP responses - debug: enable debugging messages if set to True - log_level: logging level (use constants from logging module) - options: None or optparse.OptionParser object to provide additional options - parent_proxy: parent proxy, either IP address or hostname, with optional - port (example: 'myproxy.local:8080') - log_file: bool, if True a log file will be generated for each request - """ - # create HTTP server - self.address = address - self.port = port - self.server = wsgiserver.CherryPyWSGIServer((address, port), - self._proxy_app, server_name=server_name) - # thread local variables to store request/response data per thread: - self.req = threading.local() - self.resp = threading.local() - if debug: - self.debug = self._debug_enabled - self.debug_mode = True - else: - self.debug = self._debug_disabled - self.debug_mode = False - self.options = options - # initialize logging - self.log_level = log_level -## self.log = logging.getLogger('CProxy') -## self.log.setLevel(log_level) - # default logger - self.req.log = logging.getLogger('CProxy') - self.req.log.setLevel(log_level) - self.log_file = log_file - # parent proxy - self.parent_proxy = parent_proxy - if parent_proxy: - self.debug('Using parent proxy: %s' % parent_proxy) - - - def start(self): - """ - start proxy server - """ - self.req.log.info('CherryProxy listening on %s:%d (press Ctrl+C to stop)' - % (self.address, self.port)) - self.server.start() - - - def stop(self): - """ - stop proxy server - """ - self.server.stop() - self.req.log.info('CherryProxy stopped.') - - - def filter_request_headers(self): - """ - Method to be overridden: - Called to analyse/filter/modify the request received from the client, - before reading the full request with its body if there is one, - before it is sent to the server. - - This method may call set_response() if the request needs to be blocked - before being sent to the server. - - The following attributes can be read and MODIFIED: - self.req.headers: dictionary of HTTP headers, with lowercase names - self.req.method: HTTP method, e.g. 'GET', 'POST', etc - self.req.scheme: protocol from URL, e.g. 'http' or 'https' - self.req.netloc: IP address or hostname of server, with optional - port, for example 'www.google.com' or '1.2.3.4:8000' - self.req.path: path in URL, for example '/folder/index.html' - self.req.query: query string, found after question mark in URL - - The following attributes can be READ only: - self.req.environ: dictionary of request attributes following WSGI - format (PEP 333) - self.req.url: partial URL containing 'path?query' - self.req.full_url: full URL containing 'scheme:netloc/path?query' - self.req.length: length of request data in bytes, 0 if none - self.req.content_type: content-type, for example 'text/html' - self.req.charset: charset, for example 'UTF-8' - self.req.url_filename: filename extracted from URL path - """ - pass - - - def filter_request(self): - """ - Method to be overridden: - Called to analyse/filter/modify the request received from the client, - after reading the full request with its body if there is one, - before it is sent to the server. - - This method may call set_response() if the request needs to be blocked - before being sent to the server. - - The following attributes can be read and MODIFIED: - self.req.data: data sent with the request (POST or PUT) - (and also all listed in filter_request_headers) - """ - pass - - - def filter_response_headers(self): - """ - Method to be overridden: - Called to analyse/filter/modify the response received from the server, - before reading the full response with its body if there is one, - before it is sent back to the client. - - This method may call set_response() if the response needs to be blocked - (e.g. replaced by a simple response) before being sent to the client. - - The following attributes can be read and MODIFIED: - self.resp.status: int, HTTP status of response, e.g. 200, 404, etc - self.resp.reason: reason string, e.g. 'OK', 'Not Found', etc - self.resp.headers: response headers, list of (header, value) tuples - - The following attributes can be READ only: - self.resp.httpconn: httplib.HTTPConnection object - self.resp.response: httplib.HTTPResponse object - self.resp.content_type: content-type of response - self.resp.charset: charset of response - self.resp.content_disp_filename: filename extracted from - content-disposition header - """ - pass - - - def filter_response(self): - """ - Method to be overridden: - Called to analyse/filter/modify the response received from the server, - after reading the full response with its body if there is one, - before it is sent back to the client. - - This method may call set_response() if the response needs to be blocked - (e.g. replaced by a simple response) before being sent to the client. - - The following attributes can be read and MODIFIED: - self.resp.data: data sent with the response - (and also all listed in filter_response_headers) - """ - pass - - - def set_response(self, status, reason=None, data=None, content_type='text/plain'): - """ - set a HTTP response to be sent to the client instead of the one from - the server. - - - status: int, HTTP status code (see RFC 2616) - - reason: str, optional text for the response line, standard text by default - - data: str, optional body for the response, default="status reason" - - content_type: str, content-type corresponding to data - """ - self.resp.status = status - if reason is None: - # get standard text corresponding to status - reason = httplib.responses[status] - self.resp.reason = reason - if data is None: - data = "%d %s" % (status, reason) - self.resp.data = data - # reset all headers - self.resp.headers = [] - self.resp.headers.append(('content-type', content_type)) - #self.resp.headers.append(('content-length', str(len(data)))) # not required - - - def set_response_forbidden(self, status=403, reason='Forbidden', - data=None, content_type='text/plain'): - """ - set a HTTP 403 Forbidden response to be sent to the client instead of - the one from the server. - - - status: int, HTTP status code (see RFC 2616) - - reason: str, optional text for the response line, standard text by default - - data: str, optional body for the response, default="status reason" - - content_type: str, content-type corresponding to data - """ - self.set_response(status, reason=reason, data=data, - content_type=content_type) - - - def _proxy_app(self, environ, start_response): - """ - main method called when a request is received from a client - (WSGI application) - """ - self._init_request_response() - # parse request headers: - self._parse_request(environ) - # filter request headers before reading the request body: - self.filter_request_headers() - # check if the response was set by filter_request_headers, else continue: - if self.resp.status is None: - # if request has data, read it: - self._read_request_body() - # filter request before sending it to server: - self.filter_request() - # check if the response was set by filter_request, else forward to server: - if self.resp.status is None: - # send request to server: - self._send_request() - # parse response headers: - self._parse_response() - # filter response headers before reading the response body: - self.filter_response_headers() - # check if the response was set by filter_response_headers, else continue: - # (here we need to check if resp.data is still None) - if self.resp.data is None: - # read the response body - self._read_response_body() - # filter response before sending it to client: - self.filter_response() - # For now we always close the connection, even if the client sends - # several requests in one connection: - # (not optimal performance-wise, but simpler to code) - if self.resp.httpconn is not None: - self.resp.httpconn.close() - # send response to client: - self._send_response(start_response) - # send response body: - return [self.resp.data] - - - def _init_request_response(self): - """ - Initialize variables when a new request is received - """ - # set request id (simply increase number at each request) - with self._lock_reqid: - self._reqid +=1 - self.req.reqid = self._reqid - reqname = 'Req%05d' % self.req.reqid - # set a logger for each request - # check if there is already one, set by a previous request: - if not hasattr(self.req, 'log'): - # no logger yet for this thread, create one: - self.req.log = logging.getLogger(reqname) - self.req.log.setLevel(self.log_level) - if self.log_file: - # force logging level to debug: - self.req.log.setLevel(logging.DEBUG) - # close and remove file handler from previous request: - if hasattr(self.req, '_log_handler'): - self.req._log_handler.close() - self.req.log.removeHandler(self.req._log_handler) - # add a file handler for this request - self.req._log_handler = logging.FileHandler(reqname+'.log', 'w') - self.req.log.addHandler(self.req._log_handler) - # request variables - self.req.environ = {} - self.req.headers = {} - self.req.method = None - self.req.scheme = None - self.req.netloc = None - self.req.path = None - self.req.query = None - self.req.url = None - self.req.length = 0 - self.req.content_type = None - self.req.charset = None - self.req.url_filename = None - self.req.data = None - # response variables - self.resp.httpconn = None - self.resp.response = None - self.resp.status = None - self.resp.reason = None - self.resp.headers = [] # httplib headers is a list of (header, value) tuples - self.resp.content_type = None - self.resp.charset = None - self.resp.content_disp_filename = None - self.resp.data = None - - - def _parse_request(self, environ): - """ - parse a request received from a client - """ - self.req.environ = environ - #self.debug('_'*50) - self.debug('REQUEST RECEIVED FROM CLIENT:') - self.debug('req.environ = %s' % environ) - #for env in environ: - # self.debug('%s: %s' % (env, environ[env])) - #print environ - # convert WSGI environ to a dict of HTTP headers: - self.req.headers = {} - for h in environ: - if h.startswith('HTTP_'): - hname = h[5:].replace('_', '-').lower() - self.req.headers[hname] = environ[h] - self.debug('req.headers = %s' % self.req.headers) - # content-type and content-length are stored differently, without "HTTP_" - # (cf. CherryPy WSGIServer source code or WSGI specs) - #self.req.headers['content-type'] = self.req.environ.get('CONTENT_TYPE', None) - #self.req.headers['content-length'] = self.req.environ.get('CONTENT_LENGTH', None) - #print headers - self.req.method = environ['REQUEST_METHOD'] # GET, POST, HEAD, etc - self.req.scheme = environ['wsgi.url_scheme'] # http - self.req.netloc = environ['SERVER_NAME'] # www.server.com[:80] - self.req.path = environ['PATH_INFO'] # /folder/index.html - self.req.query = environ['QUERY_STRING'] - # URL=/path?query used when forwarding directly to the server - self.req.url = urlparse.urlunsplit( - ('', '', self.req.path, self.req.query, '')) - self.debug('req.url = %s' % self.req.url) - # full URL used when forwarding to a parent proxy - self.req.full_url = urlparse.urlunsplit( - (self.req.scheme, self.req.netloc, self.req.path, self.req.query, '')) - self.debug('req.full_url = %s' % self.req.full_url) - # parse content-type and charset: - # see RFC 2616: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.17 - #ct = self.req.headers.get('content-type', None) - ct = self.req.environ.get('CONTENT_TYPE', None) - if ct is not None: - if ';' in ct: - ct, charset = ct.split(';', 1) - self.req.content_type = ct.strip() - self.req.charset = charset.strip() - else: - self.req.content_type = ct.strip() - self.debug('req.content_type = %s' % repr(self.req.content_type)) - self.debug('req.charset = %s' % repr(self.req.charset)) - #self.debug('- '*25) - self.req.log.info('Request %s %s' % (self.req.method, self.req.full_url)) - # init values before reading request body - self.req.length = 0 - self.req.data = None - # Reject request if method or scheme is not allowed: - if self.req.method in BLACKLIST_METHODS: - # here I use 501 "not implemented" rather than 405 or 401, because - # it seems to be the most appropriate response according to RFC 2616. - # see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html - msg = 'Method "%s" not supported.' % self.req.method - self.set_response(501, reason=msg) - self.req.log.error(msg) - if self.req.scheme in BLACKLIST_SCHEMES: - msg = 'Scheme "%s" not supported.' % self.req.scheme - self.set_response(501, reason=msg) - self.req.log.error(msg) - - - - def _read_request_body(self): - """ - read the request body if available - """ - environ = self.req.environ - # if request has data, read it: - if 'CONTENT_LENGTH' in environ: - self.req.length = int(environ['CONTENT_LENGTH']) - self.debug('REQUEST BODY: content-length=%d' % self.req.length) - self.req.data = environ['wsgi.input'].read(self.req.length) - self.debug(self.req.data) - else: - self.req.length = 0 - self.req.data = None - self.debug('No request body.') - - - def _send_request(self): - """ - forward a request received from a client to the server - Get the response (but not the response body yet) - """ - #TODO: reconstruct URL from its elements (if they were changed) - #self.debug('- '*25) - if self.parent_proxy: - # if a parent proxy is specified, this is the address to connect to: - netloc = self.parent_proxy - # and the URL is the full one: - url = self.req.full_url - self.debug('sending request to the parent proxy: %s - %s' % (netloc, url)) - else: - # if no parent proxy is specified, we connect directly to the server: - netloc = self.req.netloc - # and the URL is the full one: - url = self.req.url - self.debug('sending request directly to the server: %s - %s' % (netloc, url)) - # send request to server or proxy: - self.resp.httpconn = httplib.HTTPConnection(netloc) -## if self.debug_mode: -## self.resp.httpconn.set_debuglevel(1) - #TODO: handle connection errors - self.resp.httpconn.request(self.req.method, url, - body=self.req.data, headers=self.req.headers) - self.resp.response = self.resp.httpconn.getresponse() - self.resp.status = self.resp.response.status - self.resp.reason = self.resp.response.reason - status = "%d %s" % (self.resp.status, self.resp.reason) #'200 OK' - #self.debug('- '*25) - self.debug('RESPONSE RECEIVED FROM SERVER: %s' % status) - - - def _parse_response(self): - """ - parse a request received from a client - """ - self.resp.headers = self.resp.response.getheaders() #[('Content-type','text/plain')] - for h in self.resp.headers: - self.debug(' - %s: %s' % (h[0], h[1])) - # parse content-type and charset: - # using mimetools.Message.gettype() on HTTPResponse.msg - self.resp.content_type = self.resp.response.msg.gettype().lower() - self.debug('resp.content_type = %s' % repr(self.resp.content_type)) -## ct = self.resp.headers.get('content-type', None) -## if ';' in ct: -## ct, charset = ct.split(';', 1) -## self.req.content_type = ct.strip() -## self.req.charset = charset.strip() -## elif ct is not None: -## self.req.content_type = ct.strip() - self.req.log.info('Response %s %s' % (self.resp.status, self.resp.reason)) - - - - def _read_response_body(self): - """ - read the response body and close the connection - """ - # TODO: check content-length? - self.resp.data = self.resp.response.read() -## print '- '*39 -## print repr(self.data) - - - def _send_response(self, start_response): - """ - send the response with headers (but no body yet) - """ - status = "%d %s" % (self.resp.status, self.resp.reason) #'200 OK' - #self.debug('- '*25) - self.debug('RESPONSE SENT TO CLIENT:') - self.debug(status) - for h in self.resp.headers: - self.debug(' - %s: %s' % (h[0], h[1])) - start_response(status, self.resp.headers) - - - def _debug_enabled(self, string): - """ - debug method when debug mode is enabled - """ - #print string - #self.req.log.debug(string) - self.req.log.debug(string) - - def _debug_disabled(self, string): - """ - debug method when debug mode is disabled (does nothing) - """ - pass - - -#=== MAIN ===================================================================== - -def main(cproxy=CherryProxy, optionparser=None): - """ - main function for testing purposes from the command-line. - - cproxy: optional proxy class derived from CherryProxy - optionparser: optional optparse.OptionParser object to provide additional - command line options - """ - if optionparser is None: - import optparse - parser = optparse.OptionParser() - else: - parser = optionparser - parser.add_option("-p", "--port", dest="port", type='int', default=8070, - help="port for HTTP proxy, 8070 by default") - parser.add_option("-a", "--address", dest="address", default='localhost', - help="IP address of interface for HTTP proxy (0.0.0.0 for all, default=localhost)") - parser.add_option("-f", "--forward", dest="proxy", default=None, - help="Forward requests to parent proxy, specified as hostname[:port] or IP address[:port]") - parser.add_option("-v", "--verbose", action="store_true", dest="verbose", - help='Verbose mode, display debugging messages') - parser.add_option("-l", "--log", action="store_true", dest="log_file", - help='Log each request to a separate file (use with -v)') - (options, args) = parser.parse_args() - if len(args) != 0: - parser.error("incorrect number of arguments") - - # simple CherryProxy without filter: - debug=False - log_level = logging.INFO - try: - if options.verbose: - debug=True - log_level = logging.DEBUG - except: - pass - - # setup logging - logging.basicConfig(format='%(name)s-%(thread)05d: %(levelname)-8s %(message)s', - level=log_level) - - print __doc__ - proxy = cproxy(address=options.address, port=options.port, - debug=debug, log_level=log_level, options=options, - parent_proxy=options.proxy, log_file=options.log_file) - while True: - try: - proxy.start() - except KeyboardInterrupt: - proxy.stop() - sys.exit() - - -if __name__ == '__main__': +""" +CherryProxy + +a lightweight HTTP proxy based on the CherryPy WSGI server and httplib, +extensible for content analysis and filtering. + +AUTHOR: Philippe Lagadec (decalage at laposte dot net) + +PROJECT WEBSITE: http://www.decalage.info/python/cherryproxy + +LICENSE: + +Copyright (c) 2008-2011, Philippe Lagadec (decalage at laposte dot net) + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above copyright +notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + +Usage: +- either run this script directly for a demo, and use localhost:8070 as proxy. +- or create a class inheriting from CherryProxy and implement the methods + filter_request and filter_response as desired. See the example scripts for + more information. + +Usage as a script: CherryProxy.py [options] + +Options: + -h, --help show this help message and exit + -p PORT, --port=PORT port for HTTP proxy, 8070 by default + -a ADDRESS, --address=ADDRESS + IP address of interface for HTTP proxy (0.0.0.0 for + all, default=localhost) + -f PROXY, --forward=PROXY + Forward requests to parent proxy, specified as + hostname[:port] or IP address[:port] + -v, --verbose Verbose mode, display debugging messages + -l, --log Log each request to a separate file (use with -v) +""" + +#------------------------------------------------------------------------------ +# CHANGELOG: +# 2008-11-01 v0.01 PL: - first version +# 2008-11-02 v0.02 PL: - extensible CherryProxy class instead of functions +# 2009-05-05 v0.03 PL: - added comments and license +# - option to set server name in constructor +# 2009-05-06 v0.04 PL: - forward request body to server +# 2010-04-25 v0.05 PL: - moved nozip demo to separate script +# - debug option to enable/disable debug output +# 2011-09-03 v0.06 PL: - replaced attributes by thread local variables to +# support multithreading +# 2011-09-07 v0.07 PL: - use logging instead of print for debugging +# - split proxy_app into several methods +# - close each http connection to server +# 2011-09-15 v0.08 PL: - command-line options +# 2011-09-21 v0.09 PL: - separate main function (to be used in examples) +# 2011-09-24 v0.10 PL: - renamed adapt to filter +# - added methods to send response without forwarding +# request to server +# 2011-09-30 v0.11 PL: - added methods to filter headers before reading body +# 2011-11-15 v0.12 PL: - moved and renamed private methods with an underscore +# - added option to use a parent proxy +# 2011-11-29 v0.13 PL: - new option (-l) to log each request to a file +# - black list to block unsupported HTTP methods and schemes + +#------------------------------------------------------------------------------ +# TODO: +# + log_file: fix debug level without -v, add formatter +# + CLI option to dump request and response data using repr() +# + disable debug options +# + fix examples, using CT+filename, blocking some requests +# + simple doc describing API +# + methods to parse useful headers: content-type, content-disposition, etc +# http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.17 +# http://www.ietf.org/rfc/rfc2183.txt +# + method to disable logging (if log_level=None) and to add a dummy handler +# + init option to enable debug messages or not +# + force connection close and remove keep-alive on server side +# + close connection on server side when needed +# + _send_request: reconstruct URL from its elements (if they were changed) +# + _send_request: handle connection errors + +# TODO LATER: +# - update CherryPy WSGI server to the latest version 3.2.2 +# - option to save each request and response (before and after adaptation) to a file +# - later, reuse http connection when no connection close header or keep-alive +# - option to uncompress body when gzip/deflate/compress is used +# - support HTTPS +# ? config file to set options? +# ? use urllib2 instead of httplib? + +#--- IMPORTS ------------------------------------------------------------------ + +from cherrypy import wsgiserver +import urlparse, urllib2, httplib, sys, threading, logging + + +#--- CONSTANTS ---------------------------------------------------------------- + +__version__ = '0.13' + +SERVER_NAME = 'CherryProxy/%s' % __version__ + +# Not supported methods and schemes +BLACKLIST_METHODS = ['CONNECT'] +BLACKLIST_SCHEMES = ['https'] + + +#=== CLASSES ================================================================== + +class CherryProxy (object): + """ + CherryProxy: class implementing a filtering HTTP proxy + + To use it, create a class inheriting from CherryProxy and implement the + methods filter_request and filter_response as desired. + Then call the start method to start the proxy. + Note: the logging module needs to be initialized before creating a + CherryProxy object. + See the example scripts for more information. + """ + + # class variables: + # unique id for each request + _reqid = 0 + _lock_reqid = threading.Lock() + + def __init__(self, address='localhost', port=8070, server_name=SERVER_NAME, + debug=False, log_level=logging.INFO, options=None, parent_proxy=None, + log_file=False): + """ + CherryProxy constructor + + address: IP address of interface to listen to, or 0.0.0.0 for all + (localhost by default) + port: TCP port for the proxy (8070 by default) + server_name: server name used in HTTP responses + debug: enable debugging messages if set to True + log_level: logging level (use constants from logging module) + options: None or optparse.OptionParser object to provide additional options + parent_proxy: parent proxy, either IP address or hostname, with optional + port (example: 'myproxy.local:8080') + log_file: bool, if True a log file will be generated for each request + """ + # create HTTP server + self.address = address + self.port = port + self.server = wsgiserver.CherryPyWSGIServer((address, port), + self._proxy_app, server_name=server_name) + # thread local variables to store request/response data per thread: + self.req = threading.local() + self.resp = threading.local() + if debug: + self.debug = self._debug_enabled + self.debug_mode = True + else: + self.debug = self._debug_disabled + self.debug_mode = False + self.options = options + # initialize logging + self.log_level = log_level +## self.log = logging.getLogger('CProxy') +## self.log.setLevel(log_level) + # default logger + self.req.log = logging.getLogger('CProxy') + self.req.log.setLevel(log_level) + self.log_file = log_file + # parent proxy + self.parent_proxy = parent_proxy + if parent_proxy: + self.debug('Using parent proxy: %s' % parent_proxy) + + + def start(self): + """ + start proxy server + """ + self.req.log.info('CherryProxy listening on %s:%d (press Ctrl+C to stop)' + % (self.address, self.port)) + self.server.start() + + + def stop(self): + """ + stop proxy server + """ + self.server.stop() + self.req.log.info('CherryProxy stopped.') + + + def filter_request_headers(self): + """ + Method to be overridden: + Called to analyse/filter/modify the request received from the client, + before reading the full request with its body if there is one, + before it is sent to the server. + + This method may call set_response() if the request needs to be blocked + before being sent to the server. + + The following attributes can be read and MODIFIED: + self.req.headers: dictionary of HTTP headers, with lowercase names + self.req.method: HTTP method, e.g. 'GET', 'POST', etc + self.req.scheme: protocol from URL, e.g. 'http' or 'https' + self.req.netloc: IP address or hostname of server, with optional + port, for example 'www.google.com' or '1.2.3.4:8000' + self.req.path: path in URL, for example '/folder/index.html' + self.req.query: query string, found after question mark in URL + + The following attributes can be READ only: + self.req.environ: dictionary of request attributes following WSGI + format (PEP 333) + self.req.url: partial URL containing 'path?query' + self.req.full_url: full URL containing 'scheme:netloc/path?query' + self.req.length: length of request data in bytes, 0 if none + self.req.content_type: content-type, for example 'text/html' + self.req.charset: charset, for example 'UTF-8' + self.req.url_filename: filename extracted from URL path + """ + pass + + + def filter_request(self): + """ + Method to be overridden: + Called to analyse/filter/modify the request received from the client, + after reading the full request with its body if there is one, + before it is sent to the server. + + This method may call set_response() if the request needs to be blocked + before being sent to the server. + + The following attributes can be read and MODIFIED: + self.req.data: data sent with the request (POST or PUT) + (and also all listed in filter_request_headers) + """ + pass + + + def filter_response_headers(self): + """ + Method to be overridden: + Called to analyse/filter/modify the response received from the server, + before reading the full response with its body if there is one, + before it is sent back to the client. + + This method may call set_response() if the response needs to be blocked + (e.g. replaced by a simple response) before being sent to the client. + + The following attributes can be read and MODIFIED: + self.resp.status: int, HTTP status of response, e.g. 200, 404, etc + self.resp.reason: reason string, e.g. 'OK', 'Not Found', etc + self.resp.headers: response headers, list of (header, value) tuples + + The following attributes can be READ only: + self.resp.httpconn: httplib.HTTPConnection object + self.resp.response: httplib.HTTPResponse object + self.resp.content_type: content-type of response + self.resp.charset: charset of response + self.resp.content_disp_filename: filename extracted from + content-disposition header + """ + pass + + + def filter_response(self): + """ + Method to be overridden: + Called to analyse/filter/modify the response received from the server, + after reading the full response with its body if there is one, + before it is sent back to the client. + + This method may call set_response() if the response needs to be blocked + (e.g. replaced by a simple response) before being sent to the client. + + The following attributes can be read and MODIFIED: + self.resp.data: data sent with the response + (and also all listed in filter_response_headers) + """ + pass + + + def set_response(self, status, reason=None, data=None, content_type='text/plain'): + """ + set a HTTP response to be sent to the client instead of the one from + the server. + + - status: int, HTTP status code (see RFC 2616) + - reason: str, optional text for the response line, standard text by default + - data: str, optional body for the response, default="status reason" + - content_type: str, content-type corresponding to data + """ + self.resp.status = status + if reason is None: + # get standard text corresponding to status + reason = httplib.responses[status] + self.resp.reason = reason + if data is None: + data = "%d %s" % (status, reason) + self.resp.data = data + # reset all headers + self.resp.headers = [] + self.resp.headers.append(('content-type', content_type)) + #self.resp.headers.append(('content-length', str(len(data)))) # not required + + + def set_response_forbidden(self, status=403, reason='Forbidden', + data=None, content_type='text/plain'): + """ + set a HTTP 403 Forbidden response to be sent to the client instead of + the one from the server. + + - status: int, HTTP status code (see RFC 2616) + - reason: str, optional text for the response line, standard text by default + - data: str, optional body for the response, default="status reason" + - content_type: str, content-type corresponding to data + """ + self.set_response(status, reason=reason, data=data, + content_type=content_type) + + + def _proxy_app(self, environ, start_response): + """ + main method called when a request is received from a client + (WSGI application) + """ + self._init_request_response() + # parse request headers: + self._parse_request(environ) + # filter request headers before reading the request body: + self.filter_request_headers() + # check if the response was set by filter_request_headers, else continue: + if self.resp.status is None: + # if request has data, read it: + self._read_request_body() + # filter request before sending it to server: + self.filter_request() + # check if the response was set by filter_request, else forward to server: + if self.resp.status is None: + # send request to server: + self._send_request() + # parse response headers: + self._parse_response() + # filter response headers before reading the response body: + self.filter_response_headers() + # check if the response was set by filter_response_headers, else continue: + # (here we need to check if resp.data is still None) + if self.resp.data is None: + # read the response body + self._read_response_body() + # filter response before sending it to client: + self.filter_response() + # For now we always close the connection, even if the client sends + # several requests in one connection: + # (not optimal performance-wise, but simpler to code) + if self.resp.httpconn is not None: + self.resp.httpconn.close() + # send response to client: + self._send_response(start_response) + # send response body: + return [self.resp.data] + + + def _init_request_response(self): + """ + Initialize variables when a new request is received + """ + # set request id (simply increase number at each request) + with self._lock_reqid: + self._reqid +=1 + self.req.reqid = self._reqid + reqname = 'Req%05d' % self.req.reqid + # set a logger for each request + # check if there is already one, set by a previous request: + if not hasattr(self.req, 'log'): + # no logger yet for this thread, create one: + self.req.log = logging.getLogger(reqname) + self.req.log.setLevel(self.log_level) + if self.log_file: + # force logging level to debug: + self.req.log.setLevel(logging.DEBUG) + # close and remove file handler from previous request: + if hasattr(self.req, '_log_handler'): + self.req._log_handler.close() + self.req.log.removeHandler(self.req._log_handler) + # add a file handler for this request + self.req._log_handler = logging.FileHandler(reqname+'.log', 'w') + self.req.log.addHandler(self.req._log_handler) + # request variables + self.req.environ = {} + self.req.headers = {} + self.req.method = None + self.req.scheme = None + self.req.netloc = None + self.req.path = None + self.req.query = None + self.req.url = None + self.req.length = 0 + self.req.content_type = None + self.req.charset = None + self.req.url_filename = None + self.req.data = None + # response variables + self.resp.httpconn = None + self.resp.response = None + self.resp.status = None + self.resp.reason = None + self.resp.headers = [] # httplib headers is a list of (header, value) tuples + self.resp.content_type = None + self.resp.charset = None + self.resp.content_disp_filename = None + self.resp.data = None + + + def _parse_request(self, environ): + """ + parse a request received from a client + """ + self.req.environ = environ + #self.debug('_'*50) + self.debug('REQUEST RECEIVED FROM CLIENT:') + self.debug('req.environ = %s' % environ) + #for env in environ: + # self.debug('%s: %s' % (env, environ[env])) + #print environ + # convert WSGI environ to a dict of HTTP headers: + self.req.headers = {} + for h in environ: + if h.startswith('HTTP_'): + hname = h[5:].replace('_', '-').lower() + self.req.headers[hname] = environ[h] + self.debug('req.headers = %s' % self.req.headers) + # content-type and content-length are stored differently, without "HTTP_" + # (cf. CherryPy WSGIServer source code or WSGI specs) + #self.req.headers['content-type'] = self.req.environ.get('CONTENT_TYPE', None) + #self.req.headers['content-length'] = self.req.environ.get('CONTENT_LENGTH', None) + #print headers + self.req.method = environ['REQUEST_METHOD'] # GET, POST, HEAD, etc + self.req.scheme = environ['wsgi.url_scheme'] # http + self.req.netloc = environ['SERVER_NAME'] # www.server.com[:80] + self.req.path = environ['PATH_INFO'] # /folder/index.html + self.req.query = environ['QUERY_STRING'] + # URL=/path?query used when forwarding directly to the server + self.req.url = urlparse.urlunsplit( + ('', '', self.req.path, self.req.query, '')) + self.debug('req.url = %s' % self.req.url) + # full URL used when forwarding to a parent proxy + self.req.full_url = urlparse.urlunsplit( + (self.req.scheme, self.req.netloc, self.req.path, self.req.query, '')) + self.debug('req.full_url = %s' % self.req.full_url) + # parse content-type and charset: + # see RFC 2616: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.17 + #ct = self.req.headers.get('content-type', None) + ct = self.req.environ.get('CONTENT_TYPE', None) + if ct is not None: + if ';' in ct: + ct, charset = ct.split(';', 1) + self.req.content_type = ct.strip() + self.req.charset = charset.strip() + else: + self.req.content_type = ct.strip() + self.debug('req.content_type = %s' % repr(self.req.content_type)) + self.debug('req.charset = %s' % repr(self.req.charset)) + #self.debug('- '*25) + self.req.log.info('Request %s %s' % (self.req.method, self.req.full_url)) + # init values before reading request body + self.req.length = 0 + self.req.data = None + # Reject request if method or scheme is not allowed: + if self.req.method in BLACKLIST_METHODS: + # here I use 501 "not implemented" rather than 405 or 401, because + # it seems to be the most appropriate response according to RFC 2616. + # see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html + msg = 'Method "%s" not supported.' % self.req.method + self.set_response(501, reason=msg) + self.req.log.error(msg) + if self.req.scheme in BLACKLIST_SCHEMES: + msg = 'Scheme "%s" not supported.' % self.req.scheme + self.set_response(501, reason=msg) + self.req.log.error(msg) + + + + def _read_request_body(self): + """ + read the request body if available + """ + environ = self.req.environ + # if request has data, read it: + if 'CONTENT_LENGTH' in environ: + self.req.length = int(environ['CONTENT_LENGTH']) + self.debug('REQUEST BODY: content-length=%d' % self.req.length) + self.req.data = environ['wsgi.input'].read(self.req.length) + self.debug(self.req.data) + else: + self.req.length = 0 + self.req.data = None + self.debug('No request body.') + + + def _send_request(self): + """ + forward a request received from a client to the server + Get the response (but not the response body yet) + """ + #TODO: reconstruct URL from its elements (if they were changed) + #self.debug('- '*25) + if self.parent_proxy: + # if a parent proxy is specified, this is the address to connect to: + netloc = self.parent_proxy + # and the URL is the full one: + url = self.req.full_url + self.debug('sending request to the parent proxy: %s - %s' % (netloc, url)) + else: + # if no parent proxy is specified, we connect directly to the server: + netloc = self.req.netloc + # and the URL is the full one: + url = self.req.url + self.debug('sending request directly to the server: %s - %s' % (netloc, url)) + # send request to server or proxy: + self.resp.httpconn = httplib.HTTPConnection(netloc) +## if self.debug_mode: +## self.resp.httpconn.set_debuglevel(1) + #TODO: handle connection errors + self.resp.httpconn.request(self.req.method, url, + body=self.req.data, headers=self.req.headers) + self.resp.response = self.resp.httpconn.getresponse() + self.resp.status = self.resp.response.status + self.resp.reason = self.resp.response.reason + status = "%d %s" % (self.resp.status, self.resp.reason) #'200 OK' + #self.debug('- '*25) + self.debug('RESPONSE RECEIVED FROM SERVER: %s' % status) + + + def _parse_response(self): + """ + parse a request received from a client + """ + self.resp.headers = self.resp.response.getheaders() #[('Content-type','text/plain')] + for h in self.resp.headers: + self.debug(' - %s: %s' % (h[0], h[1])) + # parse content-type and charset: + # using mimetools.Message.gettype() on HTTPResponse.msg + self.resp.content_type = self.resp.response.msg.gettype().lower() + self.debug('resp.content_type = %s' % repr(self.resp.content_type)) +## ct = self.resp.headers.get('content-type', None) +## if ';' in ct: +## ct, charset = ct.split(';', 1) +## self.req.content_type = ct.strip() +## self.req.charset = charset.strip() +## elif ct is not None: +## self.req.content_type = ct.strip() + self.req.log.info('Response %s %s' % (self.resp.status, self.resp.reason)) + + + + def _read_response_body(self): + """ + read the response body and close the connection + """ + # TODO: check content-length? + self.resp.data = self.resp.response.read() +## print '- '*39 +## print repr(self.data) + + + def _send_response(self, start_response): + """ + send the response with headers (but no body yet) + """ + status = "%d %s" % (self.resp.status, self.resp.reason) #'200 OK' + #self.debug('- '*25) + self.debug('RESPONSE SENT TO CLIENT:') + self.debug(status) + for h in self.resp.headers: + self.debug(' - %s: %s' % (h[0], h[1])) + start_response(status, self.resp.headers) + + + def _debug_enabled(self, string): + """ + debug method when debug mode is enabled + """ + #print string + #self.req.log.debug(string) + self.req.log.debug(string) + + def _debug_disabled(self, string): + """ + debug method when debug mode is disabled (does nothing) + """ + pass + + +#=== MAIN ===================================================================== + +def main(cproxy=CherryProxy, optionparser=None): + """ + main function for testing purposes from the command-line. + + cproxy: optional proxy class derived from CherryProxy + optionparser: optional optparse.OptionParser object to provide additional + command line options + """ + if optionparser is None: + import optparse + parser = optparse.OptionParser() + else: + parser = optionparser + parser.add_option("-p", "--port", dest="port", type='int', default=8070, + help="port for HTTP proxy, 8070 by default") + parser.add_option("-a", "--address", dest="address", default='localhost', + help="IP address of interface for HTTP proxy (0.0.0.0 for all, default=localhost)") + parser.add_option("-f", "--forward", dest="proxy", default=None, + help="Forward requests to parent proxy, specified as hostname[:port] or IP address[:port]") + parser.add_option("-v", "--verbose", action="store_true", dest="verbose", + help='Verbose mode, display debugging messages') + parser.add_option("-l", "--log", action="store_true", dest="log_file", + help='Log each request to a separate file (use with -v)') + (options, args) = parser.parse_args() + if len(args) != 0: + parser.error("incorrect number of arguments") + + # simple CherryProxy without filter: + debug=False + log_level = logging.INFO + try: + if options.verbose: + debug=True + log_level = logging.DEBUG + except: + pass + + # setup logging + logging.basicConfig(format='%(name)s-%(thread)05d: %(levelname)-8s %(message)s', + level=log_level) + + print(__doc__) + proxy = cproxy(address=options.address, port=options.port, + debug=debug, log_level=log_level, options=options, + parent_proxy=options.proxy, log_file=options.log_file) + while True: + try: + proxy.start() + except KeyboardInterrupt: + proxy.stop() + sys.exit() + + +if __name__ == '__main__': main() \ No newline at end of file diff --git a/thirdparty/cherryproxy/cherrypy/wsgiserver/__init__.py b/thirdparty/cherryproxy/cherrypy/wsgiserver/__init__.py index 76fb86f..6c1a559 100644 --- a/thirdparty/cherryproxy/cherrypy/wsgiserver/__init__.py +++ b/thirdparty/cherryproxy/cherrypy/wsgiserver/__init__.py @@ -1,1794 +1,1793 @@ -"""A high-speed, production ready, thread pooled, generic WSGI server. - -Simplest example on how to use this module directly -(without using CherryPy's application machinery): - - from cherrypy import wsgiserver - - def my_crazy_app(environ, start_response): - status = '200 OK' - response_headers = [('Content-type','text/plain')] - start_response(status, response_headers) - return ['Hello world!\n'] - - server = wsgiserver.CherryPyWSGIServer( - ('0.0.0.0', 8070), my_crazy_app, - server_name='www.cherrypy.example') - -The CherryPy WSGI server can serve as many WSGI applications -as you want in one instance by using a WSGIPathInfoDispatcher: - - d = WSGIPathInfoDispatcher({'/': my_crazy_app, '/blog': my_blog_app}) - server = wsgiserver.CherryPyWSGIServer(('0.0.0.0', 80), d) - -Want SSL support? Just set these attributes: - - server.ssl_certificate = - server.ssl_private_key = - - if __name__ == '__main__': - try: - server.start() - except KeyboardInterrupt: - server.stop() - -This won't call the CherryPy engine (application side) at all, only the -WSGI server, which is independant from the rest of CherryPy. Don't -let the name "CherryPyWSGIServer" throw you; the name merely reflects -its origin, not its coupling. - -For those of you wanting to understand internals of this module, here's the -basic call flow. The server's listening thread runs a very tight loop, -sticking incoming connections onto a Queue: - - server = CherryPyWSGIServer(...) - server.start() - while True: - tick() - # This blocks until a request comes in: - child = socket.accept() - conn = HTTPConnection(child, ...) - server.requests.put(conn) - -Worker threads are kept in a pool and poll the Queue, popping off and then -handling each connection in turn. Each connection can consist of an arbitrary -number of requests and their responses, so we run a nested loop: - - while True: - conn = server.requests.get() - conn.communicate() - -> while True: - req = HTTPRequest(...) - req.parse_request() - -> # Read the Request-Line, e.g. "GET /page HTTP/1.1" - req.rfile.readline() - req.read_headers() - req.respond() - -> response = wsgi_app(...) - try: - for chunk in response: - if chunk: - req.write(chunk) - finally: - if hasattr(response, "close"): - response.close() - if req.close_connection: - return -""" - - -import base64 -import os -import Queue -import re -quoted_slash = re.compile("(?i)%2F") -import rfc822 -import socket -try: - import cStringIO as StringIO -except ImportError: - import StringIO - -_fileobject_uses_str_type = isinstance(socket._fileobject(None)._rbuf, basestring) - -import sys -import threading -import time -import traceback -from urllib import unquote -from urlparse import urlparse -import warnings - -try: - from OpenSSL import SSL - from OpenSSL import crypto -except ImportError: - SSL = None - -import errno - -def plat_specific_errors(*errnames): - """Return error numbers for all errors in errnames on this platform. - - The 'errno' module contains different global constants depending on - the specific platform (OS). This function will return the list of - numeric values for a given list of potential names. - """ - errno_names = dir(errno) - nums = [getattr(errno, k) for k in errnames if k in errno_names] - # de-dupe the list - return dict.fromkeys(nums).keys() - -socket_error_eintr = plat_specific_errors("EINTR", "WSAEINTR") - -socket_errors_to_ignore = plat_specific_errors( - "EPIPE", - "EBADF", "WSAEBADF", - "ENOTSOCK", "WSAENOTSOCK", - "ETIMEDOUT", "WSAETIMEDOUT", - "ECONNREFUSED", "WSAECONNREFUSED", - "ECONNRESET", "WSAECONNRESET", - "ECONNABORTED", "WSAECONNABORTED", - "ENETRESET", "WSAENETRESET", - "EHOSTDOWN", "EHOSTUNREACH", - ) -socket_errors_to_ignore.append("timed out") - -socket_errors_nonblocking = plat_specific_errors( - 'EAGAIN', 'EWOULDBLOCK', 'WSAEWOULDBLOCK') - -comma_separated_headers = ['ACCEPT', 'ACCEPT-CHARSET', 'ACCEPT-ENCODING', - 'ACCEPT-LANGUAGE', 'ACCEPT-RANGES', 'ALLOW', 'CACHE-CONTROL', - 'CONNECTION', 'CONTENT-ENCODING', 'CONTENT-LANGUAGE', 'EXPECT', - 'IF-MATCH', 'IF-NONE-MATCH', 'PRAGMA', 'PROXY-AUTHENTICATE', 'TE', - 'TRAILER', 'TRANSFER-ENCODING', 'UPGRADE', 'VARY', 'VIA', 'WARNING', - 'WWW-AUTHENTICATE'] - - -class WSGIPathInfoDispatcher(object): - """A WSGI dispatcher for dispatch based on the PATH_INFO. - - apps: a dict or list of (path_prefix, app) pairs. - """ - - def __init__(self, apps): - try: - apps = apps.items() - except AttributeError: - pass - - # Sort the apps by len(path), descending - apps.sort() - apps.reverse() - - # The path_prefix strings must start, but not end, with a slash. - # Use "" instead of "/". - self.apps = [(p.rstrip("/"), a) for p, a in apps] - - def __call__(self, environ, start_response): - path = environ["PATH_INFO"] or "/" - for p, app in self.apps: - # The apps list should be sorted by length, descending. - if path.startswith(p + "/") or path == p: - environ = environ.copy() - environ["SCRIPT_NAME"] = environ["SCRIPT_NAME"] + p - environ["PATH_INFO"] = path[len(p):] - return app(environ, start_response) - - start_response('404 Not Found', [('Content-Type', 'text/plain'), - ('Content-Length', '0')]) - return [''] - - -class MaxSizeExceeded(Exception): - pass - -class SizeCheckWrapper(object): - """Wraps a file-like object, raising MaxSizeExceeded if too large.""" - - def __init__(self, rfile, maxlen): - self.rfile = rfile - self.maxlen = maxlen - self.bytes_read = 0 - - def _check_length(self): - if self.maxlen and self.bytes_read > self.maxlen: - raise MaxSizeExceeded() - - def read(self, size=None): - data = self.rfile.read(size) - self.bytes_read += len(data) - self._check_length() - return data - - def readline(self, size=None): - if size is not None: - data = self.rfile.readline(size) - self.bytes_read += len(data) - self._check_length() - return data - - # User didn't specify a size ... - # We read the line in chunks to make sure it's not a 100MB line ! - res = [] - while True: - data = self.rfile.readline(256) - self.bytes_read += len(data) - self._check_length() - res.append(data) - # See http://www.cherrypy.org/ticket/421 - if len(data) < 256 or data[-1:] == "\n": - return ''.join(res) - - def readlines(self, sizehint=0): - # Shamelessly stolen from StringIO - total = 0 - lines = [] - line = self.readline() - while line: - lines.append(line) - total += len(line) - if 0 < sizehint <= total: - break - line = self.readline() - return lines - - def close(self): - self.rfile.close() - - def __iter__(self): - return self - - def next(self): - data = self.rfile.next() - self.bytes_read += len(data) - self._check_length() - return data - - -class HTTPRequest(object): - """An HTTP Request (and response). - - A single HTTP connection may consist of multiple request/response pairs. - - send: the 'send' method from the connection's socket object. - wsgi_app: the WSGI application to call. - environ: a partial WSGI environ (server and connection entries). - The caller MUST set the following entries: - * All wsgi.* entries, including .input - * SERVER_NAME and SERVER_PORT - * Any SSL_* entries - * Any custom entries like REMOTE_ADDR and REMOTE_PORT - * SERVER_SOFTWARE: the value to write in the "Server" response header. - * ACTUAL_SERVER_PROTOCOL: the value to write in the Status-Line of - the response. From RFC 2145: "An HTTP server SHOULD send a - response version equal to the highest version for which the - server is at least conditionally compliant, and whose major - version is less than or equal to the one received in the - request. An HTTP server MUST NOT send a version for which - it is not at least conditionally compliant." - - outheaders: a list of header tuples to write in the response. - ready: when True, the request has been parsed and is ready to begin - generating the response. When False, signals the calling Connection - that the response should not be generated and the connection should - close. - close_connection: signals the calling Connection that the request - should close. This does not imply an error! The client and/or - server may each request that the connection be closed. - chunked_write: if True, output will be encoded with the "chunked" - transfer-coding. This value is set automatically inside - send_headers. - """ - - max_request_header_size = 0 - max_request_body_size = 0 - - def __init__(self, wfile, environ, wsgi_app): - self.rfile = environ['wsgi.input'] - self.wfile = wfile - self.environ = environ.copy() - self.wsgi_app = wsgi_app - - self.ready = False - self.started_response = False - self.status = "" - self.outheaders = [] - self.sent_headers = False - self.close_connection = False - self.chunked_write = False - - def parse_request(self): - """Parse the next HTTP request start-line and message-headers.""" - self.rfile.maxlen = self.max_request_header_size - self.rfile.bytes_read = 0 - - try: - self._parse_request() - except MaxSizeExceeded: - self.simple_response("413 Request Entity Too Large") - return - - def _parse_request(self): - # HTTP/1.1 connections are persistent by default. If a client - # requests a page, then idles (leaves the connection open), - # then rfile.readline() will raise socket.error("timed out"). - # Note that it does this based on the value given to settimeout(), - # and doesn't need the client to request or acknowledge the close - # (although your TCP stack might suffer for it: cf Apache's history - # with FIN_WAIT_2). - request_line = self.rfile.readline() - if not request_line: - # Force self.ready = False so the connection will close. - self.ready = False - return - - if request_line == "\r\n": - # RFC 2616 sec 4.1: "...if the server is reading the protocol - # stream at the beginning of a message and receives a CRLF - # first, it should ignore the CRLF." - # But only ignore one leading line! else we enable a DoS. - request_line = self.rfile.readline() - if not request_line: - self.ready = False - return - - environ = self.environ - - try: - method, path, req_protocol = request_line.strip().split(" ", 2) - except ValueError: - self.simple_response(400, "Malformed Request-Line") - return - - environ["REQUEST_METHOD"] = method - - # path may be an abs_path (including "http://host.domain.tld"); - scheme, location, path, params, qs, frag = urlparse(path) - - if frag: - self.simple_response("400 Bad Request", - "Illegal #fragment in Request-URI.") - return - - if scheme: - environ["wsgi.url_scheme"] = scheme - if params: - path = path + ";" + params - - environ["SCRIPT_NAME"] = "" - - # Unquote the path+params (e.g. "/this%20path" -> "this path"). - # http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1.2 - # - # But note that "...a URI must be separated into its components - # before the escaped characters within those components can be - # safely decoded." http://www.ietf.org/rfc/rfc2396.txt, sec 2.4.2 - atoms = [unquote(x) for x in quoted_slash.split(path)] - path = "%2F".join(atoms) - environ["PATH_INFO"] = path - - # Note that, like wsgiref and most other WSGI servers, - # we unquote the path but not the query string. - environ["QUERY_STRING"] = qs - - # Compare request and server HTTP protocol versions, in case our - # server does not support the requested protocol. Limit our output - # to min(req, server). We want the following output: - # request server actual written supported response - # protocol protocol response protocol feature set - # a 1.0 1.0 1.0 1.0 - # b 1.0 1.1 1.1 1.0 - # c 1.1 1.0 1.0 1.0 - # d 1.1 1.1 1.1 1.1 - # Notice that, in (b), the response will be "HTTP/1.1" even though - # the client only understands 1.0. RFC 2616 10.5.6 says we should - # only return 505 if the _major_ version is different. - rp = int(req_protocol[5]), int(req_protocol[7]) - server_protocol = environ["ACTUAL_SERVER_PROTOCOL"] - sp = int(server_protocol[5]), int(server_protocol[7]) - if sp[0] != rp[0]: - self.simple_response("505 HTTP Version Not Supported") - return - # Bah. "SERVER_PROTOCOL" is actually the REQUEST protocol. - environ["SERVER_PROTOCOL"] = req_protocol - self.response_protocol = "HTTP/%s.%s" % min(rp, sp) - - # If the Request-URI was an absoluteURI, use its location atom. - if location: - environ["SERVER_NAME"] = location - - # then all the http headers - try: - self.read_headers() - except ValueError, ex: - self.simple_response("400 Bad Request", repr(ex.args)) - return - - mrbs = self.max_request_body_size - if mrbs and int(environ.get("CONTENT_LENGTH", 0)) > mrbs: - self.simple_response("413 Request Entity Too Large") - return - - # Persistent connection support - if self.response_protocol == "HTTP/1.1": - # Both server and client are HTTP/1.1 - if environ.get("HTTP_CONNECTION", "") == "close": - self.close_connection = True - else: - # Either the server or client (or both) are HTTP/1.0 - if environ.get("HTTP_CONNECTION", "") != "Keep-Alive": - self.close_connection = True - - # Transfer-Encoding support - te = None - if self.response_protocol == "HTTP/1.1": - te = environ.get("HTTP_TRANSFER_ENCODING") - if te: - te = [x.strip().lower() for x in te.split(",") if x.strip()] - - self.chunked_read = False - - if te: - for enc in te: - if enc == "chunked": - self.chunked_read = True - else: - # Note that, even if we see "chunked", we must reject - # if there is an extension we don't recognize. - self.simple_response("501 Unimplemented") - self.close_connection = True - return - - # From PEP 333: - # "Servers and gateways that implement HTTP 1.1 must provide - # transparent support for HTTP 1.1's "expect/continue" mechanism. - # This may be done in any of several ways: - # 1. Respond to requests containing an Expect: 100-continue request - # with an immediate "100 Continue" response, and proceed normally. - # 2. Proceed with the request normally, but provide the application - # with a wsgi.input stream that will send the "100 Continue" - # response if/when the application first attempts to read from - # the input stream. The read request must then remain blocked - # until the client responds. - # 3. Wait until the client decides that the server does not support - # expect/continue, and sends the request body on its own. - # (This is suboptimal, and is not recommended.) - # - # We used to do 3, but are now doing 1. Maybe we'll do 2 someday, - # but it seems like it would be a big slowdown for such a rare case. - if environ.get("HTTP_EXPECT", "") == "100-continue": - self.simple_response(100) - - self.ready = True - - def read_headers(self): - """Read header lines from the incoming stream.""" - environ = self.environ - - while True: - line = self.rfile.readline() - if not line: - # No more data--illegal end of headers - raise ValueError("Illegal end of headers.") - - if line == '\r\n': - # Normal end of headers - break - - if line[0] in ' \t': - # It's a continuation line. - v = line.strip() - else: - k, v = line.split(":", 1) - k, v = k.strip().upper(), v.strip() - envname = "HTTP_" + k.replace("-", "_") - - if k in comma_separated_headers: - existing = environ.get(envname) - if existing: - v = ", ".join((existing, v)) - environ[envname] = v - - ct = environ.pop("HTTP_CONTENT_TYPE", None) - if ct is not None: - environ["CONTENT_TYPE"] = ct - cl = environ.pop("HTTP_CONTENT_LENGTH", None) - if cl is not None: - environ["CONTENT_LENGTH"] = cl - - def decode_chunked(self): - """Decode the 'chunked' transfer coding.""" - cl = 0 - data = StringIO.StringIO() - while True: - line = self.rfile.readline().strip().split(";", 1) - chunk_size = int(line.pop(0), 16) - if chunk_size <= 0: - break -## if line: chunk_extension = line[0] - cl += chunk_size - data.write(self.rfile.read(chunk_size)) - crlf = self.rfile.read(2) - if crlf != "\r\n": - self.simple_response("400 Bad Request", - "Bad chunked transfer coding " - "(expected '\\r\\n', got %r)" % crlf) - return - - # Grab any trailer headers - self.read_headers() - - data.seek(0) - self.environ["wsgi.input"] = data - self.environ["CONTENT_LENGTH"] = str(cl) or "" - return True - - def respond(self): - """Call the appropriate WSGI app and write its iterable output.""" - # Set rfile.maxlen to ensure we don't read past Content-Length. - # This will also be used to read the entire request body if errors - # are raised before the app can read the body. - if self.chunked_read: - # If chunked, Content-Length will be 0. - self.rfile.maxlen = self.max_request_body_size - else: - cl = int(self.environ.get("CONTENT_LENGTH", 0)) - if self.max_request_body_size: - self.rfile.maxlen = min(cl, self.max_request_body_size) - else: - self.rfile.maxlen = cl - self.rfile.bytes_read = 0 - - try: - self._respond() - except MaxSizeExceeded: - if not self.sent_headers: - self.simple_response("413 Request Entity Too Large") - return - - def _respond(self): - if self.chunked_read: - if not self.decode_chunked(): - self.close_connection = True - return - - response = self.wsgi_app(self.environ, self.start_response) - try: - for chunk in response: - # "The start_response callable must not actually transmit - # the response headers. Instead, it must store them for the - # server or gateway to transmit only after the first - # iteration of the application return value that yields - # a NON-EMPTY string, or upon the application's first - # invocation of the write() callable." (PEP 333) - if chunk: - self.write(chunk) - finally: - if hasattr(response, "close"): - response.close() - - if (self.ready and not self.sent_headers): - self.sent_headers = True - self.send_headers() - if self.chunked_write: - self.wfile.sendall("0\r\n\r\n") - - def simple_response(self, status, msg=""): - """Write a simple response back to the client.""" - status = str(status) - buf = ["%s %s\r\n" % (self.environ['ACTUAL_SERVER_PROTOCOL'], status), - "Content-Length: %s\r\n" % len(msg), - "Content-Type: text/plain\r\n"] - - if status[:3] == "413" and self.response_protocol == 'HTTP/1.1': - # Request Entity Too Large - self.close_connection = True - buf.append("Connection: close\r\n") - - buf.append("\r\n") - if msg: - buf.append(msg) - - try: - self.wfile.sendall("".join(buf)) - except socket.error, x: - if x.args[0] not in socket_errors_to_ignore: - raise - - def start_response(self, status, headers, exc_info = None): - """WSGI callable to begin the HTTP response.""" - # "The application may call start_response more than once, - # if and only if the exc_info argument is provided." - if self.started_response and not exc_info: - raise AssertionError("WSGI start_response called a second " - "time with no exc_info.") - - # "if exc_info is provided, and the HTTP headers have already been - # sent, start_response must raise an error, and should raise the - # exc_info tuple." - if self.sent_headers: - try: - raise exc_info[0], exc_info[1], exc_info[2] - finally: - exc_info = None - - self.started_response = True - self.status = status - self.outheaders.extend(headers) - return self.write - - def write(self, chunk): - """WSGI callable to write unbuffered data to the client. - - This method is also used internally by start_response (to write - data from the iterable returned by the WSGI application). - """ - if not self.started_response: - raise AssertionError("WSGI write called before start_response.") - - if not self.sent_headers: - self.sent_headers = True - self.send_headers() - - if self.chunked_write and chunk: - buf = [hex(len(chunk))[2:], "\r\n", chunk, "\r\n"] - self.wfile.sendall("".join(buf)) - else: - self.wfile.sendall(chunk) - - def send_headers(self): - """Assert, process, and send the HTTP response message-headers.""" - hkeys = [key.lower() for key, value in self.outheaders] - status = int(self.status[:3]) - - if status == 413: - # Request Entity Too Large. Close conn to avoid garbage. - self.close_connection = True - elif "content-length" not in hkeys: - # "All 1xx (informational), 204 (no content), - # and 304 (not modified) responses MUST NOT - # include a message-body." So no point chunking. - if status < 200 or status in (204, 205, 304): - pass - else: - if (self.response_protocol == 'HTTP/1.1' - and self.environ["REQUEST_METHOD"] != 'HEAD'): - # Use the chunked transfer-coding - self.chunked_write = True - self.outheaders.append(("Transfer-Encoding", "chunked")) - else: - # Closing the conn is the only way to determine len. - self.close_connection = True - - if "connection" not in hkeys: - if self.response_protocol == 'HTTP/1.1': - # Both server and client are HTTP/1.1 or better - if self.close_connection: - self.outheaders.append(("Connection", "close")) - else: - # Server and/or client are HTTP/1.0 - if not self.close_connection: - self.outheaders.append(("Connection", "Keep-Alive")) - - if (not self.close_connection) and (not self.chunked_read): - # Read any remaining request body data on the socket. - # "If an origin server receives a request that does not include an - # Expect request-header field with the "100-continue" expectation, - # the request includes a request body, and the server responds - # with a final status code before reading the entire request body - # from the transport connection, then the server SHOULD NOT close - # the transport connection until it has read the entire request, - # or until the client closes the connection. Otherwise, the client - # might not reliably receive the response message. However, this - # requirement is not be construed as preventing a server from - # defending itself against denial-of-service attacks, or from - # badly broken client implementations." - size = self.rfile.maxlen - self.rfile.bytes_read - if size > 0: - self.rfile.read(size) - - if "date" not in hkeys: - self.outheaders.append(("Date", rfc822.formatdate())) - - if "server" not in hkeys: - self.outheaders.append(("Server", self.environ['SERVER_SOFTWARE'])) - - buf = [self.environ['ACTUAL_SERVER_PROTOCOL'], " ", self.status, "\r\n"] - try: - buf += [k + ": " + v + "\r\n" for k, v in self.outheaders] - except TypeError: - if not isinstance(k, str): - raise TypeError("WSGI response header key %r is not a string.") - if not isinstance(v, str): - raise TypeError("WSGI response header value %r is not a string.") - else: - raise - buf.append("\r\n") - self.wfile.sendall("".join(buf)) - - -class NoSSLError(Exception): - """Exception raised when a client speaks HTTP to an HTTPS socket.""" - pass - - -class FatalSSLAlert(Exception): - """Exception raised when the SSL implementation signals a fatal alert.""" - pass - - -if not _fileobject_uses_str_type: - class CP_fileobject(socket._fileobject): - """Faux file object attached to a socket object.""" - - def sendall(self, data): - """Sendall for non-blocking sockets.""" - while data: - try: - bytes_sent = self.send(data) - data = data[bytes_sent:] - except socket.error, e: - if e.args[0] not in socket_errors_nonblocking: - raise - - def send(self, data): - return self._sock.send(data) - - def flush(self): - if self._wbuf: - buffer = "".join(self._wbuf) - self._wbuf = [] - self.sendall(buffer) - - def recv(self, size): - while True: - try: - return self._sock.recv(size) - except socket.error, e: - if (e.args[0] not in socket_errors_nonblocking - and e.args[0] not in socket_error_eintr): - raise - - def read(self, size=-1): - # Use max, disallow tiny reads in a loop as they are very inefficient. - # We never leave read() with any leftover data from a new recv() call - # in our internal buffer. - rbufsize = max(self._rbufsize, self.default_bufsize) - # Our use of StringIO rather than lists of string objects returned by - # recv() minimizes memory usage and fragmentation that occurs when - # rbufsize is large compared to the typical return value of recv(). - buf = self._rbuf - buf.seek(0, 2) # seek end - if size < 0: - # Read until EOF - self._rbuf = StringIO.StringIO() # reset _rbuf. we consume it via buf. - while True: - data = self.recv(rbufsize) - if not data: - break - buf.write(data) - return buf.getvalue() - else: - # Read until size bytes or EOF seen, whichever comes first - buf_len = buf.tell() - if buf_len >= size: - # Already have size bytes in our buffer? Extract and return. - buf.seek(0) - rv = buf.read(size) - self._rbuf = StringIO.StringIO() - self._rbuf.write(buf.read()) - return rv - - self._rbuf = StringIO.StringIO() # reset _rbuf. we consume it via buf. - while True: - left = size - buf_len - # recv() will malloc the amount of memory given as its - # parameter even though it often returns much less data - # than that. The returned data string is short lived - # as we copy it into a StringIO and free it. This avoids - # fragmentation issues on many platforms. - data = self.recv(left) - if not data: - break - n = len(data) - if n == size and not buf_len: - # Shortcut. Avoid buffer data copies when: - # - We have no data in our buffer. - # AND - # - Our call to recv returned exactly the - # number of bytes we were asked to read. - return data - if n == left: - buf.write(data) - del data # explicit free - break - assert n <= left, "recv(%d) returned %d bytes" % (left, n) - buf.write(data) - buf_len += n - del data # explicit free - #assert buf_len == buf.tell() - return buf.getvalue() - - def readline(self, size=-1): - buf = self._rbuf - buf.seek(0, 2) # seek end - if buf.tell() > 0: - # check if we already have it in our buffer - buf.seek(0) - bline = buf.readline(size) - if bline.endswith('\n') or len(bline) == size: - self._rbuf = StringIO.StringIO() - self._rbuf.write(buf.read()) - return bline - del bline - if size < 0: - # Read until \n or EOF, whichever comes first - if self._rbufsize <= 1: - # Speed up unbuffered case - buf.seek(0) - buffers = [buf.read()] - self._rbuf = StringIO.StringIO() # reset _rbuf. we consume it via buf. - data = None - recv = self.recv - while data != "\n": - data = recv(1) - if not data: - break - buffers.append(data) - return "".join(buffers) - - buf.seek(0, 2) # seek end - self._rbuf = StringIO.StringIO() # reset _rbuf. we consume it via buf. - while True: - data = self.recv(self._rbufsize) - if not data: - break - nl = data.find('\n') - if nl >= 0: - nl += 1 - buf.write(data[:nl]) - self._rbuf.write(data[nl:]) - del data - break - buf.write(data) - return buf.getvalue() - else: - # Read until size bytes or \n or EOF seen, whichever comes first - buf.seek(0, 2) # seek end - buf_len = buf.tell() - if buf_len >= size: - buf.seek(0) - rv = buf.read(size) - self._rbuf = StringIO.StringIO() - self._rbuf.write(buf.read()) - return rv - self._rbuf = StringIO.StringIO() # reset _rbuf. we consume it via buf. - while True: - data = self.recv(self._rbufsize) - if not data: - break - left = size - buf_len - # did we just receive a newline? - nl = data.find('\n', 0, left) - if nl >= 0: - nl += 1 - # save the excess data to _rbuf - self._rbuf.write(data[nl:]) - if buf_len: - buf.write(data[:nl]) - break - else: - # Shortcut. Avoid data copy through buf when returning - # a substring of our first recv(). - return data[:nl] - n = len(data) - if n == size and not buf_len: - # Shortcut. Avoid data copy through buf when - # returning exactly all of our first recv(). - return data - if n >= left: - buf.write(data[:left]) - self._rbuf.write(data[left:]) - break - buf.write(data) - buf_len += n - #assert buf_len == buf.tell() - return buf.getvalue() - -else: - class CP_fileobject(socket._fileobject): - """Faux file object attached to a socket object.""" - - def sendall(self, data): - """Sendall for non-blocking sockets.""" - while data: - try: - bytes_sent = self.send(data) - data = data[bytes_sent:] - except socket.error, e: - if e.args[0] not in socket_errors_nonblocking: - raise - - def send(self, data): - return self._sock.send(data) - - def flush(self): - if self._wbuf: - buffer = "".join(self._wbuf) - self._wbuf = [] - self.sendall(buffer) - - def recv(self, size): - while True: - try: - return self._sock.recv(size) - except socket.error, e: - if (e.args[0] not in socket_errors_nonblocking - and e.args[0] not in socket_error_eintr): - raise - - def read(self, size=-1): - if size < 0: - # Read until EOF - buffers = [self._rbuf] - self._rbuf = "" - if self._rbufsize <= 1: - recv_size = self.default_bufsize - else: - recv_size = self._rbufsize - - while True: - data = self.recv(recv_size) - if not data: - break - buffers.append(data) - return "".join(buffers) - else: - # Read until size bytes or EOF seen, whichever comes first - data = self._rbuf - buf_len = len(data) - if buf_len >= size: - self._rbuf = data[size:] - return data[:size] - buffers = [] - if data: - buffers.append(data) - self._rbuf = "" - while True: - left = size - buf_len - recv_size = max(self._rbufsize, left) - data = self.recv(recv_size) - if not data: - break - buffers.append(data) - n = len(data) - if n >= left: - self._rbuf = data[left:] - buffers[-1] = data[:left] - break - buf_len += n - return "".join(buffers) - - def readline(self, size=-1): - data = self._rbuf - if size < 0: - # Read until \n or EOF, whichever comes first - if self._rbufsize <= 1: - # Speed up unbuffered case - assert data == "" - buffers = [] - while data != "\n": - data = self.recv(1) - if not data: - break - buffers.append(data) - return "".join(buffers) - nl = data.find('\n') - if nl >= 0: - nl += 1 - self._rbuf = data[nl:] - return data[:nl] - buffers = [] - if data: - buffers.append(data) - self._rbuf = "" - while True: - data = self.recv(self._rbufsize) - if not data: - break - buffers.append(data) - nl = data.find('\n') - if nl >= 0: - nl += 1 - self._rbuf = data[nl:] - buffers[-1] = data[:nl] - break - return "".join(buffers) - else: - # Read until size bytes or \n or EOF seen, whichever comes first - nl = data.find('\n', 0, size) - if nl >= 0: - nl += 1 - self._rbuf = data[nl:] - return data[:nl] - buf_len = len(data) - if buf_len >= size: - self._rbuf = data[size:] - return data[:size] - buffers = [] - if data: - buffers.append(data) - self._rbuf = "" - while True: - data = self.recv(self._rbufsize) - if not data: - break - buffers.append(data) - left = size - buf_len - nl = data.find('\n', 0, left) - if nl >= 0: - nl += 1 - self._rbuf = data[nl:] - buffers[-1] = data[:nl] - break - n = len(data) - if n >= left: - self._rbuf = data[left:] - buffers[-1] = data[:left] - break - buf_len += n - return "".join(buffers) - - -class SSL_fileobject(CP_fileobject): - """SSL file object attached to a socket object.""" - - ssl_timeout = 3 - ssl_retry = .01 - - def _safe_call(self, is_reader, call, *args, **kwargs): - """Wrap the given call with SSL error-trapping. - - is_reader: if False EOF errors will be raised. If True, EOF errors - will return "" (to emulate normal sockets). - """ - start = time.time() - while True: - try: - return call(*args, **kwargs) - except SSL.WantReadError: - # Sleep and try again. This is dangerous, because it means - # the rest of the stack has no way of differentiating - # between a "new handshake" error and "client dropped". - # Note this isn't an endless loop: there's a timeout below. - time.sleep(self.ssl_retry) - except SSL.WantWriteError: - time.sleep(self.ssl_retry) - except SSL.SysCallError, e: - if is_reader and e.args == (-1, 'Unexpected EOF'): - return "" - - errnum = e.args[0] - if is_reader and errnum in socket_errors_to_ignore: - return "" - raise socket.error(errnum) - except SSL.Error, e: - if is_reader and e.args == (-1, 'Unexpected EOF'): - return "" - - thirdarg = None - try: - thirdarg = e.args[0][0][2] - except IndexError: - pass - - if thirdarg == 'http request': - # The client is talking HTTP to an HTTPS server. - raise NoSSLError() - raise FatalSSLAlert(*e.args) - except: - raise - - if time.time() - start > self.ssl_timeout: - raise socket.timeout("timed out") - - def recv(self, *args, **kwargs): - buf = [] - r = super(SSL_fileobject, self).recv - while True: - data = self._safe_call(True, r, *args, **kwargs) - buf.append(data) - p = self._sock.pending() - if not p: - return "".join(buf) - - def sendall(self, *args, **kwargs): - return self._safe_call(False, super(SSL_fileobject, self).sendall, *args, **kwargs) - - def send(self, *args, **kwargs): - return self._safe_call(False, super(SSL_fileobject, self).send, *args, **kwargs) - - -class HTTPConnection(object): - """An HTTP connection (active socket). - - socket: the raw socket object (usually TCP) for this connection. - wsgi_app: the WSGI application for this server/connection. - environ: a WSGI environ template. This will be copied for each request. - - rfile: a fileobject for reading from the socket. - send: a function for writing (+ flush) to the socket. - """ - - rbufsize = -1 - RequestHandlerClass = HTTPRequest - environ = {"wsgi.version": (1, 0), - "wsgi.url_scheme": "http", - "wsgi.multithread": True, - "wsgi.multiprocess": False, - "wsgi.run_once": False, - "wsgi.errors": sys.stderr, - } - - def __init__(self, sock, wsgi_app, environ): - self.socket = sock - self.wsgi_app = wsgi_app - - # Copy the class environ into self. - self.environ = self.environ.copy() - self.environ.update(environ) - - if SSL and isinstance(sock, SSL.ConnectionType): - timeout = sock.gettimeout() - self.rfile = SSL_fileobject(sock, "rb", self.rbufsize) - self.rfile.ssl_timeout = timeout - self.wfile = SSL_fileobject(sock, "wb", -1) - self.wfile.ssl_timeout = timeout - else: - self.rfile = CP_fileobject(sock, "rb", self.rbufsize) - self.wfile = CP_fileobject(sock, "wb", -1) - - # Wrap wsgi.input but not HTTPConnection.rfile itself. - # We're also not setting maxlen yet; we'll do that separately - # for headers and body for each iteration of self.communicate - # (if maxlen is 0 the wrapper doesn't check length). - self.environ["wsgi.input"] = SizeCheckWrapper(self.rfile, 0) - - def communicate(self): - """Read each request and respond appropriately.""" - try: - while True: - # (re)set req to None so that if something goes wrong in - # the RequestHandlerClass constructor, the error doesn't - # get written to the previous request. - req = None - req = self.RequestHandlerClass(self.wfile, self.environ, - self.wsgi_app) - - # This order of operations should guarantee correct pipelining. - req.parse_request() - if not req.ready: - return - - req.respond() - if req.close_connection: - return - - except socket.error, e: - errnum = e.args[0] - if errnum == 'timed out': - if req and not req.sent_headers: - req.simple_response("408 Request Timeout") - elif errnum not in socket_errors_to_ignore: - if req and not req.sent_headers: - req.simple_response("500 Internal Server Error", - format_exc()) - return - except (KeyboardInterrupt, SystemExit): - raise - except FatalSSLAlert, e: - # Close the connection. - return - except NoSSLError: - if req and not req.sent_headers: - # Unwrap our wfile - req.wfile = CP_fileobject(self.socket._sock, "wb", -1) - req.simple_response("400 Bad Request", - "The client sent a plain HTTP request, but " - "this server only speaks HTTPS on this port.") - self.linger = True - except Exception, e: - if req and not req.sent_headers: - req.simple_response("500 Internal Server Error", format_exc()) - - linger = False - - def close(self): - """Close the socket underlying this connection.""" - self.rfile.close() - - if not self.linger: - # Python's socket module does NOT call close on the kernel socket - # when you call socket.close(). We do so manually here because we - # want this server to send a FIN TCP segment immediately. Note this - # must be called *before* calling socket.close(), because the latter - # drops its reference to the kernel socket. - self.socket._sock.close() - self.socket.close() - else: - # On the other hand, sometimes we want to hang around for a bit - # to make sure the client has a chance to read our entire - # response. Skipping the close() calls here delays the FIN - # packet until the socket object is garbage-collected later. - # Someday, perhaps, we'll do the full lingering_close that - # Apache does, but not today. - pass - - -def format_exc(limit=None): - """Like print_exc() but return a string. Backport for Python 2.3.""" - try: - etype, value, tb = sys.exc_info() - return ''.join(traceback.format_exception(etype, value, tb, limit)) - finally: - etype = value = tb = None - - -_SHUTDOWNREQUEST = None - -class WorkerThread(threading.Thread): - """Thread which continuously polls a Queue for Connection objects. - - server: the HTTP Server which spawned this thread, and which owns the - Queue and is placing active connections into it. - ready: a simple flag for the calling server to know when this thread - has begun polling the Queue. - - Due to the timing issues of polling a Queue, a WorkerThread does not - check its own 'ready' flag after it has started. To stop the thread, - it is necessary to stick a _SHUTDOWNREQUEST object onto the Queue - (one for each running WorkerThread). - """ - - conn = None - - def __init__(self, server): - self.ready = False - self.server = server - threading.Thread.__init__(self) - - def run(self): - try: - self.ready = True - while True: - conn = self.server.requests.get() - if conn is _SHUTDOWNREQUEST: - return - - self.conn = conn - try: - conn.communicate() - finally: - conn.close() - self.conn = None - except (KeyboardInterrupt, SystemExit), exc: - self.server.interrupt = exc - - -class ThreadPool(object): - """A Request Queue for the CherryPyWSGIServer which pools threads. - - ThreadPool objects must provide min, get(), put(obj), start() - and stop(timeout) attributes. - """ - - def __init__(self, server, min=10, max=-1): - self.server = server - self.min = min - self.max = max - self._threads = [] - self._queue = Queue.Queue() - self.get = self._queue.get - - def start(self): - """Start the pool of threads.""" - for i in xrange(self.min): - self._threads.append(WorkerThread(self.server)) - for worker in self._threads: - worker.setName("CP WSGIServer " + worker.getName()) - worker.start() - for worker in self._threads: - while not worker.ready: - time.sleep(.1) - - def _get_idle(self): - """Number of worker threads which are idle. Read-only.""" - return len([t for t in self._threads if t.conn is None]) - idle = property(_get_idle, doc=_get_idle.__doc__) - - def put(self, obj): - self._queue.put(obj) - if obj is _SHUTDOWNREQUEST: - return - - def grow(self, amount): - """Spawn new worker threads (not above self.max).""" - for i in xrange(amount): - if self.max > 0 and len(self._threads) >= self.max: - break - worker = WorkerThread(self.server) - worker.setName("CP WSGIServer " + worker.getName()) - self._threads.append(worker) - worker.start() - - def shrink(self, amount): - """Kill off worker threads (not below self.min).""" - # Grow/shrink the pool if necessary. - # Remove any dead threads from our list - for t in self._threads: - if not t.isAlive(): - self._threads.remove(t) - amount -= 1 - - if amount > 0: - for i in xrange(min(amount, len(self._threads) - self.min)): - # Put a number of shutdown requests on the queue equal - # to 'amount'. Once each of those is processed by a worker, - # that worker will terminate and be culled from our list - # in self.put. - self._queue.put(_SHUTDOWNREQUEST) - - def stop(self, timeout=5): - # Must shut down threads here so the code that calls - # this method can know when all threads are stopped. - for worker in self._threads: - self._queue.put(_SHUTDOWNREQUEST) - - # Don't join currentThread (when stop is called inside a request). - current = threading.currentThread() - while self._threads: - worker = self._threads.pop() - if worker is not current and worker.isAlive(): - try: - if timeout is None or timeout < 0: - worker.join() - else: - worker.join(timeout) - if worker.isAlive(): - # We exhausted the timeout. - # Forcibly shut down the socket. - c = worker.conn - if c and not c.rfile.closed: - if SSL and isinstance(c.socket, SSL.ConnectionType): - # pyOpenSSL.socket.shutdown takes no args - c.socket.shutdown() - else: - c.socket.shutdown(socket.SHUT_RD) - worker.join() - except (AssertionError, - # Ignore repeated Ctrl-C. - # See http://www.cherrypy.org/ticket/691. - KeyboardInterrupt), exc1: - pass - - - -class SSLConnection: - """A thread-safe wrapper for an SSL.Connection. - - *args: the arguments to create the wrapped SSL.Connection(*args). - """ - - def __init__(self, *args): - self._ssl_conn = SSL.Connection(*args) - self._lock = threading.RLock() - - for f in ('get_context', 'pending', 'send', 'write', 'recv', 'read', - 'renegotiate', 'bind', 'listen', 'connect', 'accept', - 'setblocking', 'fileno', 'shutdown', 'close', 'get_cipher_list', - 'getpeername', 'getsockname', 'getsockopt', 'setsockopt', - 'makefile', 'get_app_data', 'set_app_data', 'state_string', - 'sock_shutdown', 'get_peer_certificate', 'want_read', - 'want_write', 'set_connect_state', 'set_accept_state', - 'connect_ex', 'sendall', 'settimeout'): - exec """def %s(self, *args): - self._lock.acquire() - try: - return self._ssl_conn.%s(*args) - finally: - self._lock.release() -""" % (f, f) - - -try: - import fcntl -except ImportError: - try: - from ctypes import windll, WinError - except ImportError: - def prevent_socket_inheritance(sock): - """Dummy function, since neither fcntl nor ctypes are available.""" - pass - else: - def prevent_socket_inheritance(sock): - """Mark the given socket fd as non-inheritable (Windows).""" - if not windll.kernel32.SetHandleInformation(sock.fileno(), 1, 0): - raise WinError() -else: - def prevent_socket_inheritance(sock): - """Mark the given socket fd as non-inheritable (POSIX).""" - fd = sock.fileno() - old_flags = fcntl.fcntl(fd, fcntl.F_GETFD) - fcntl.fcntl(fd, fcntl.F_SETFD, old_flags | fcntl.FD_CLOEXEC) - - -class CherryPyWSGIServer(object): - """An HTTP server for WSGI. - - bind_addr: The interface on which to listen for connections. - For TCP sockets, a (host, port) tuple. Host values may be any IPv4 - or IPv6 address, or any valid hostname. The string 'localhost' is a - synonym for '127.0.0.1' (or '::1', if your hosts file prefers IPv6). - The string '0.0.0.0' is a special IPv4 entry meaning "any active - interface" (INADDR_ANY), and '::' is the similar IN6ADDR_ANY for - IPv6. The empty string or None are not allowed. - - For UNIX sockets, supply the filename as a string. - wsgi_app: the WSGI 'application callable'; multiple WSGI applications - may be passed as (path_prefix, app) pairs. - numthreads: the number of worker threads to create (default 10). - server_name: the string to set for WSGI's SERVER_NAME environ entry. - Defaults to socket.gethostname(). - max: the maximum number of queued requests (defaults to -1 = no limit). - request_queue_size: the 'backlog' argument to socket.listen(); - specifies the maximum number of queued connections (default 5). - timeout: the timeout in seconds for accepted connections (default 10). - - nodelay: if True (the default since 3.1), sets the TCP_NODELAY socket - option. - - protocol: the version string to write in the Status-Line of all - HTTP responses. For example, "HTTP/1.1" (the default). This - also limits the supported features used in the response. - - - SSL/HTTPS - --------- - The OpenSSL module must be importable for SSL functionality. - You can obtain it from http://pyopenssl.sourceforge.net/ - - ssl_certificate: the filename of the server SSL certificate. - ssl_privatekey: the filename of the server's private key file. - - If either of these is None (both are None by default), this server - will not use SSL. If both are given and are valid, they will be read - on server start and used in the SSL context for the listening socket. - """ - - protocol = "HTTP/1.1" - _bind_addr = "127.0.0.1" - version = "CherryPy/3.1.2" - ready = False - _interrupt = None - - nodelay = True - - ConnectionClass = HTTPConnection - environ = {} - - # Paths to certificate and private key files - ssl_certificate = None - ssl_private_key = None - - def __init__(self, bind_addr, wsgi_app, numthreads=10, server_name=None, - max=-1, request_queue_size=5, timeout=10, shutdown_timeout=5): - self.requests = ThreadPool(self, min=numthreads or 1, max=max) - - if callable(wsgi_app): - # We've been handed a single wsgi_app, in CP-2.1 style. - # Assume it's mounted at "". - self.wsgi_app = wsgi_app - else: - # We've been handed a list of (path_prefix, wsgi_app) tuples, - # so that the server can call different wsgi_apps, and also - # correctly set SCRIPT_NAME. - warnings.warn("The ability to pass multiple apps is deprecated " - "and will be removed in 3.2. You should explicitly " - "include a WSGIPathInfoDispatcher instead.", - DeprecationWarning) - self.wsgi_app = WSGIPathInfoDispatcher(wsgi_app) - - self.bind_addr = bind_addr - if not server_name: - server_name = socket.gethostname() - self.server_name = server_name - self.request_queue_size = request_queue_size - - self.timeout = timeout - self.shutdown_timeout = shutdown_timeout - - def _get_numthreads(self): - return self.requests.min - def _set_numthreads(self, value): - self.requests.min = value - numthreads = property(_get_numthreads, _set_numthreads) - - def __str__(self): - return "%s.%s(%r)" % (self.__module__, self.__class__.__name__, - self.bind_addr) - - def _get_bind_addr(self): - return self._bind_addr - def _set_bind_addr(self, value): - if isinstance(value, tuple) and value[0] in ('', None): - # Despite the socket module docs, using '' does not - # allow AI_PASSIVE to work. Passing None instead - # returns '0.0.0.0' like we want. In other words: - # host AI_PASSIVE result - # '' Y 192.168.x.y - # '' N 192.168.x.y - # None Y 0.0.0.0 - # None N 127.0.0.1 - # But since you can get the same effect with an explicit - # '0.0.0.0', we deny both the empty string and None as values. - raise ValueError("Host values of '' or None are not allowed. " - "Use '0.0.0.0' (IPv4) or '::' (IPv6) instead " - "to listen on all active interfaces.") - self._bind_addr = value - bind_addr = property(_get_bind_addr, _set_bind_addr, - doc="""The interface on which to listen for connections. - - For TCP sockets, a (host, port) tuple. Host values may be any IPv4 - or IPv6 address, or any valid hostname. The string 'localhost' is a - synonym for '127.0.0.1' (or '::1', if your hosts file prefers IPv6). - The string '0.0.0.0' is a special IPv4 entry meaning "any active - interface" (INADDR_ANY), and '::' is the similar IN6ADDR_ANY for - IPv6. The empty string or None are not allowed. - - For UNIX sockets, supply the filename as a string.""") - - def start(self): - """Run the server forever.""" - # We don't have to trap KeyboardInterrupt or SystemExit here, - # because cherrpy.server already does so, calling self.stop() for us. - # If you're using this server with another framework, you should - # trap those exceptions in whatever code block calls start(). - self._interrupt = None - - # Select the appropriate socket - if isinstance(self.bind_addr, basestring): - # AF_UNIX socket - - # So we can reuse the socket... - try: os.unlink(self.bind_addr) - except: pass - - # So everyone can access the socket... - try: os.chmod(self.bind_addr, 0777) - except: pass - - info = [(socket.AF_UNIX, socket.SOCK_STREAM, 0, "", self.bind_addr)] - else: - # AF_INET or AF_INET6 socket - # Get the correct address family for our host (allows IPv6 addresses) - host, port = self.bind_addr - try: - info = socket.getaddrinfo(host, port, socket.AF_UNSPEC, - socket.SOCK_STREAM, 0, socket.AI_PASSIVE) - except socket.gaierror: - # Probably a DNS issue. Assume IPv4. - info = [(socket.AF_INET, socket.SOCK_STREAM, 0, "", self.bind_addr)] - - self.socket = None - msg = "No socket could be created" - for res in info: - af, socktype, proto, canonname, sa = res - try: - self.bind(af, socktype, proto) - except socket.error, msg: - if self.socket: - self.socket.close() - self.socket = None - continue - break - if not self.socket: - raise socket.error, msg - - # Timeout so KeyboardInterrupt can be caught on Win32 - self.socket.settimeout(1) - self.socket.listen(self.request_queue_size) - - # Create worker threads - self.requests.start() - - self.ready = True - while self.ready: - self.tick() - if self.interrupt: - while self.interrupt is True: - # Wait for self.stop() to complete. See _set_interrupt. - time.sleep(0.1) - if self.interrupt: - raise self.interrupt - - def bind(self, family, type, proto=0): - """Create (or recreate) the actual socket object.""" - self.socket = socket.socket(family, type, proto) - prevent_socket_inheritance(self.socket) - self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - if self.nodelay: - self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) - if self.ssl_certificate and self.ssl_private_key: - if SSL is None: - raise ImportError("You must install pyOpenSSL to use HTTPS.") - - # See http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/442473 - ctx = SSL.Context(SSL.SSLv23_METHOD) - ctx.use_privatekey_file(self.ssl_private_key) - ctx.use_certificate_file(self.ssl_certificate) - self.socket = SSLConnection(ctx, self.socket) - self.populate_ssl_environ() - - # If listening on the IPV6 any address ('::' = IN6ADDR_ANY), - # activate dual-stack. See http://www.cherrypy.org/ticket/871. - if (not isinstance(self.bind_addr, basestring) - and self.bind_addr[0] == '::' and family == socket.AF_INET6): - try: - self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0) - except (AttributeError, socket.error): - # Apparently, the socket option is not available in - # this machine's TCP stack - pass - - self.socket.bind(self.bind_addr) - - def tick(self): - """Accept a new connection and put it on the Queue.""" - try: - s, addr = self.socket.accept() - prevent_socket_inheritance(s) - if not self.ready: - return - if hasattr(s, 'settimeout'): - s.settimeout(self.timeout) - - environ = self.environ.copy() - # SERVER_SOFTWARE is common for IIS. It's also helpful for - # us to pass a default value for the "Server" response header. - if environ.get("SERVER_SOFTWARE") is None: - environ["SERVER_SOFTWARE"] = "%s WSGI Server" % self.version - # set a non-standard environ entry so the WSGI app can know what - # the *real* server protocol is (and what features to support). - # See http://www.faqs.org/rfcs/rfc2145.html. - environ["ACTUAL_SERVER_PROTOCOL"] = self.protocol - environ["SERVER_NAME"] = self.server_name - - if isinstance(self.bind_addr, basestring): - # AF_UNIX. This isn't really allowed by WSGI, which doesn't - # address unix domain sockets. But it's better than nothing. - environ["SERVER_PORT"] = "" - else: - environ["SERVER_PORT"] = str(self.bind_addr[1]) - # optional values - # Until we do DNS lookups, omit REMOTE_HOST - environ["REMOTE_ADDR"] = addr[0] - environ["REMOTE_PORT"] = str(addr[1]) - - conn = self.ConnectionClass(s, self.wsgi_app, environ) - self.requests.put(conn) - except socket.timeout: - # The only reason for the timeout in start() is so we can - # notice keyboard interrupts on Win32, which don't interrupt - # accept() by default - return - except socket.error, x: - if x.args[0] in socket_error_eintr: - # I *think* this is right. EINTR should occur when a signal - # is received during the accept() call; all docs say retry - # the call, and I *think* I'm reading it right that Python - # will then go ahead and poll for and handle the signal - # elsewhere. See http://www.cherrypy.org/ticket/707. - return - if x.args[0] in socket_errors_nonblocking: - # Just try again. See http://www.cherrypy.org/ticket/479. - return - if x.args[0] in socket_errors_to_ignore: - # Our socket was closed. - # See http://www.cherrypy.org/ticket/686. - return - raise - - def _get_interrupt(self): - return self._interrupt - def _set_interrupt(self, interrupt): - self._interrupt = True - self.stop() - self._interrupt = interrupt - interrupt = property(_get_interrupt, _set_interrupt, - doc="Set this to an Exception instance to " - "interrupt the server.") - - def stop(self): - """Gracefully shutdown a server that is serving forever.""" - self.ready = False - - sock = getattr(self, "socket", None) - if sock: - if not isinstance(self.bind_addr, basestring): - # Touch our own socket to make accept() return immediately. - try: - host, port = sock.getsockname()[:2] - except socket.error, x: - if x.args[0] not in socket_errors_to_ignore: - raise - else: - # Note that we're explicitly NOT using AI_PASSIVE, - # here, because we want an actual IP to touch. - # localhost won't work if we've bound to a public IP, - # but it will if we bound to '0.0.0.0' (INADDR_ANY). - for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC, - socket.SOCK_STREAM): - af, socktype, proto, canonname, sa = res - s = None - try: - s = socket.socket(af, socktype, proto) - # See http://groups.google.com/group/cherrypy-users/ - # browse_frm/thread/bbfe5eb39c904fe0 - s.settimeout(1.0) - s.connect((host, port)) - s.close() - except socket.error: - if s: - s.close() - if hasattr(sock, "close"): - sock.close() - self.socket = None - - self.requests.stop(self.shutdown_timeout) - - def populate_ssl_environ(self): - """Create WSGI environ entries to be merged into each request.""" - cert = open(self.ssl_certificate, 'rb').read() - cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert) - ssl_environ = { - "wsgi.url_scheme": "https", - "HTTPS": "on", - # pyOpenSSL doesn't provide access to any of these AFAICT -## 'SSL_PROTOCOL': 'SSLv2', -## SSL_CIPHER string The cipher specification name -## SSL_VERSION_INTERFACE string The mod_ssl program version -## SSL_VERSION_LIBRARY string The OpenSSL program version - } - - # Server certificate attributes - ssl_environ.update({ - 'SSL_SERVER_M_VERSION': cert.get_version(), - 'SSL_SERVER_M_SERIAL': cert.get_serial_number(), -## 'SSL_SERVER_V_START': Validity of server's certificate (start time), -## 'SSL_SERVER_V_END': Validity of server's certificate (end time), - }) - - for prefix, dn in [("I", cert.get_issuer()), - ("S", cert.get_subject())]: - # X509Name objects don't seem to have a way to get the - # complete DN string. Use str() and slice it instead, - # because str(dn) == "" - dnstr = str(dn)[18:-2] - - wsgikey = 'SSL_SERVER_%s_DN' % prefix - ssl_environ[wsgikey] = dnstr - - # The DN should be of the form: /k1=v1/k2=v2, but we must allow - # for any value to contain slashes itself (in a URL). - while dnstr: - pos = dnstr.rfind("=") - dnstr, value = dnstr[:pos], dnstr[pos + 1:] - pos = dnstr.rfind("/") - dnstr, key = dnstr[:pos], dnstr[pos + 1:] - if key and value: - wsgikey = 'SSL_SERVER_%s_DN_%s' % (prefix, key) - ssl_environ[wsgikey] = value - - self.environ.update(ssl_environ) - +"""A high-speed, production ready, thread pooled, generic WSGI server. + +Simplest example on how to use this module directly +(without using CherryPy's application machinery): + + from cherrypy import wsgiserver + + def my_crazy_app(environ, start_response): + status = '200 OK' + response_headers = [('Content-type','text/plain')] + start_response(status, response_headers) + return ['Hello world!\n'] + + server = wsgiserver.CherryPyWSGIServer( + ('0.0.0.0', 8070), my_crazy_app, + server_name='www.cherrypy.example') + +The CherryPy WSGI server can serve as many WSGI applications +as you want in one instance by using a WSGIPathInfoDispatcher: + + d = WSGIPathInfoDispatcher({'/': my_crazy_app, '/blog': my_blog_app}) + server = wsgiserver.CherryPyWSGIServer(('0.0.0.0', 80), d) + +Want SSL support? Just set these attributes: + + server.ssl_certificate = + server.ssl_private_key = + + if __name__ == '__main__': + try: + server.start() + except KeyboardInterrupt: + server.stop() + +This won't call the CherryPy engine (application side) at all, only the +WSGI server, which is independant from the rest of CherryPy. Don't +let the name "CherryPyWSGIServer" throw you; the name merely reflects +its origin, not its coupling. + +For those of you wanting to understand internals of this module, here's the +basic call flow. The server's listening thread runs a very tight loop, +sticking incoming connections onto a Queue: + + server = CherryPyWSGIServer(...) + server.start() + while True: + tick() + # This blocks until a request comes in: + child = socket.accept() + conn = HTTPConnection(child, ...) + server.requests.put(conn) + +Worker threads are kept in a pool and poll the Queue, popping off and then +handling each connection in turn. Each connection can consist of an arbitrary +number of requests and their responses, so we run a nested loop: + + while True: + conn = server.requests.get() + conn.communicate() + -> while True: + req = HTTPRequest(...) + req.parse_request() + -> # Read the Request-Line, e.g. "GET /page HTTP/1.1" + req.rfile.readline() + req.read_headers() + req.respond() + -> response = wsgi_app(...) + try: + for chunk in response: + if chunk: + req.write(chunk) + finally: + if hasattr(response, "close"): + response.close() + if req.close_connection: + return +""" + + +import base64 +import os +import queue as Queue +import re +quoted_slash = re.compile("(?i)%2F") +import rfc822 +import socket +try: + import cStringIO as StringIO +except ImportError: + import io as StringIO + +_fileobject_uses_str_type = isinstance(socket._fileobject(None)._rbuf, str) + +import sys +import threading +import time +import traceback +from urllib.parse import unquote +from urllib.parse import urlparse +import warnings + +try: + from OpenSSL import SSL + from OpenSSL import crypto +except ImportError: + SSL = None + +import errno + +def plat_specific_errors(*errnames): + """Return error numbers for all errors in errnames on this platform. + + The 'errno' module contains different global constants depending on + the specific platform (OS). This function will return the list of + numeric values for a given list of potential names. + """ + errno_names = dir(errno) + nums = [getattr(errno, k) for k in errnames if k in errno_names] + # de-dupe the list + return dict.fromkeys(nums).keys() + +socket_error_eintr = plat_specific_errors("EINTR", "WSAEINTR") + +socket_errors_to_ignore = plat_specific_errors( + "EPIPE", + "EBADF", "WSAEBADF", + "ENOTSOCK", "WSAENOTSOCK", + "ETIMEDOUT", "WSAETIMEDOUT", + "ECONNREFUSED", "WSAECONNREFUSED", + "ECONNRESET", "WSAECONNRESET", + "ECONNABORTED", "WSAECONNABORTED", + "ENETRESET", "WSAENETRESET", + "EHOSTDOWN", "EHOSTUNREACH", + ) +socket_errors_to_ignore.append("timed out") + +socket_errors_nonblocking = plat_specific_errors( + 'EAGAIN', 'EWOULDBLOCK', 'WSAEWOULDBLOCK') + +comma_separated_headers = ['ACCEPT', 'ACCEPT-CHARSET', 'ACCEPT-ENCODING', + 'ACCEPT-LANGUAGE', 'ACCEPT-RANGES', 'ALLOW', 'CACHE-CONTROL', + 'CONNECTION', 'CONTENT-ENCODING', 'CONTENT-LANGUAGE', 'EXPECT', + 'IF-MATCH', 'IF-NONE-MATCH', 'PRAGMA', 'PROXY-AUTHENTICATE', 'TE', + 'TRAILER', 'TRANSFER-ENCODING', 'UPGRADE', 'VARY', 'VIA', 'WARNING', + 'WWW-AUTHENTICATE'] + + +class WSGIPathInfoDispatcher(object): + """A WSGI dispatcher for dispatch based on the PATH_INFO. + + apps: a dict or list of (path_prefix, app) pairs. + """ + + def __init__(self, apps): + try: + apps = apps.items() + except AttributeError: + pass + + # Sort the apps by len(path), descending + apps.sort() + apps.reverse() + + # The path_prefix strings must start, but not end, with a slash. + # Use "" instead of "/". + self.apps = [(p.rstrip("/"), a) for p, a in apps] + + def __call__(self, environ, start_response): + path = environ["PATH_INFO"] or "/" + for p, app in self.apps: + # The apps list should be sorted by length, descending. + if path.startswith(p + "/") or path == p: + environ = environ.copy() + environ["SCRIPT_NAME"] = environ["SCRIPT_NAME"] + p + environ["PATH_INFO"] = path[len(p):] + return app(environ, start_response) + + start_response('404 Not Found', [('Content-Type', 'text/plain'), + ('Content-Length', '0')]) + return [''] + + +class MaxSizeExceeded(Exception): + pass + +class SizeCheckWrapper(object): + """Wraps a file-like object, raising MaxSizeExceeded if too large.""" + + def __init__(self, rfile, maxlen): + self.rfile = rfile + self.maxlen = maxlen + self.bytes_read = 0 + + def _check_length(self): + if self.maxlen and self.bytes_read > self.maxlen: + raise MaxSizeExceeded() + + def read(self, size=None): + data = self.rfile.read(size) + self.bytes_read += len(data) + self._check_length() + return data + + def readline(self, size=None): + if size is not None: + data = self.rfile.readline(size) + self.bytes_read += len(data) + self._check_length() + return data + + # User didn't specify a size ... + # We read the line in chunks to make sure it's not a 100MB line ! + res = [] + while True: + data = self.rfile.readline(256) + self.bytes_read += len(data) + self._check_length() + res.append(data) + # See http://www.cherrypy.org/ticket/421 + if len(data) < 256 or data[-1:] == "\n": + return ''.join(res) + + def readlines(self, sizehint=0): + # Shamelessly stolen from StringIO + total = 0 + lines = [] + line = self.readline() + while line: + lines.append(line) + total += len(line) + if 0 < sizehint <= total: + break + line = self.readline() + return lines + + def close(self): + self.rfile.close() + + def __iter__(self): + return self + + def next(self): + data = self.rfile.next() + self.bytes_read += len(data) + self._check_length() + return data + + +class HTTPRequest(object): + """An HTTP Request (and response). + + A single HTTP connection may consist of multiple request/response pairs. + + send: the 'send' method from the connection's socket object. + wsgi_app: the WSGI application to call. + environ: a partial WSGI environ (server and connection entries). + The caller MUST set the following entries: + * All wsgi.* entries, including .input + * SERVER_NAME and SERVER_PORT + * Any SSL_* entries + * Any custom entries like REMOTE_ADDR and REMOTE_PORT + * SERVER_SOFTWARE: the value to write in the "Server" response header. + * ACTUAL_SERVER_PROTOCOL: the value to write in the Status-Line of + the response. From RFC 2145: "An HTTP server SHOULD send a + response version equal to the highest version for which the + server is at least conditionally compliant, and whose major + version is less than or equal to the one received in the + request. An HTTP server MUST NOT send a version for which + it is not at least conditionally compliant." + + outheaders: a list of header tuples to write in the response. + ready: when True, the request has been parsed and is ready to begin + generating the response. When False, signals the calling Connection + that the response should not be generated and the connection should + close. + close_connection: signals the calling Connection that the request + should close. This does not imply an error! The client and/or + server may each request that the connection be closed. + chunked_write: if True, output will be encoded with the "chunked" + transfer-coding. This value is set automatically inside + send_headers. + """ + + max_request_header_size = 0 + max_request_body_size = 0 + + def __init__(self, wfile, environ, wsgi_app): + self.rfile = environ['wsgi.input'] + self.wfile = wfile + self.environ = environ.copy() + self.wsgi_app = wsgi_app + + self.ready = False + self.started_response = False + self.status = "" + self.outheaders = [] + self.sent_headers = False + self.close_connection = False + self.chunked_write = False + + def parse_request(self): + """Parse the next HTTP request start-line and message-headers.""" + self.rfile.maxlen = self.max_request_header_size + self.rfile.bytes_read = 0 + + try: + self._parse_request() + except MaxSizeExceeded: + self.simple_response("413 Request Entity Too Large") + return + + def _parse_request(self): + # HTTP/1.1 connections are persistent by default. If a client + # requests a page, then idles (leaves the connection open), + # then rfile.readline() will raise socket.error("timed out"). + # Note that it does this based on the value given to settimeout(), + # and doesn't need the client to request or acknowledge the close + # (although your TCP stack might suffer for it: cf Apache's history + # with FIN_WAIT_2). + request_line = self.rfile.readline() + if not request_line: + # Force self.ready = False so the connection will close. + self.ready = False + return + + if request_line == "\r\n": + # RFC 2616 sec 4.1: "...if the server is reading the protocol + # stream at the beginning of a message and receives a CRLF + # first, it should ignore the CRLF." + # But only ignore one leading line! else we enable a DoS. + request_line = self.rfile.readline() + if not request_line: + self.ready = False + return + + environ = self.environ + + try: + method, path, req_protocol = request_line.strip().split(" ", 2) + except ValueError: + self.simple_response(400, "Malformed Request-Line") + return + + environ["REQUEST_METHOD"] = method + + # path may be an abs_path (including "http://host.domain.tld"); + scheme, location, path, params, qs, frag = urlparse(path) + + if frag: + self.simple_response("400 Bad Request", + "Illegal #fragment in Request-URI.") + return + + if scheme: + environ["wsgi.url_scheme"] = scheme + if params: + path = path + ";" + params + + environ["SCRIPT_NAME"] = "" + + # Unquote the path+params (e.g. "/this%20path" -> "this path"). + # http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1.2 + # + # But note that "...a URI must be separated into its components + # before the escaped characters within those components can be + # safely decoded." http://www.ietf.org/rfc/rfc2396.txt, sec 2.4.2 + atoms = [unquote(x) for x in quoted_slash.split(path)] + path = "%2F".join(atoms) + environ["PATH_INFO"] = path + + # Note that, like wsgiref and most other WSGI servers, + # we unquote the path but not the query string. + environ["QUERY_STRING"] = qs + + # Compare request and server HTTP protocol versions, in case our + # server does not support the requested protocol. Limit our output + # to min(req, server). We want the following output: + # request server actual written supported response + # protocol protocol response protocol feature set + # a 1.0 1.0 1.0 1.0 + # b 1.0 1.1 1.1 1.0 + # c 1.1 1.0 1.0 1.0 + # d 1.1 1.1 1.1 1.1 + # Notice that, in (b), the response will be "HTTP/1.1" even though + # the client only understands 1.0. RFC 2616 10.5.6 says we should + # only return 505 if the _major_ version is different. + rp = int(req_protocol[5]), int(req_protocol[7]) + server_protocol = environ["ACTUAL_SERVER_PROTOCOL"] + sp = int(server_protocol[5]), int(server_protocol[7]) + if sp[0] != rp[0]: + self.simple_response("505 HTTP Version Not Supported") + return + # Bah. "SERVER_PROTOCOL" is actually the REQUEST protocol. + environ["SERVER_PROTOCOL"] = req_protocol + self.response_protocol = "HTTP/%s.%s" % min(rp, sp) + + # If the Request-URI was an absoluteURI, use its location atom. + if location: + environ["SERVER_NAME"] = location + + # then all the http headers + try: + self.read_headers() + except ValueError as ex: + self.simple_response("400 Bad Request", repr(ex.args)) + return + + mrbs = self.max_request_body_size + if mrbs and int(environ.get("CONTENT_LENGTH", 0)) > mrbs: + self.simple_response("413 Request Entity Too Large") + return + + # Persistent connection support + if self.response_protocol == "HTTP/1.1": + # Both server and client are HTTP/1.1 + if environ.get("HTTP_CONNECTION", "") == "close": + self.close_connection = True + else: + # Either the server or client (or both) are HTTP/1.0 + if environ.get("HTTP_CONNECTION", "") != "Keep-Alive": + self.close_connection = True + + # Transfer-Encoding support + te = None + if self.response_protocol == "HTTP/1.1": + te = environ.get("HTTP_TRANSFER_ENCODING") + if te: + te = [x.strip().lower() for x in te.split(",") if x.strip()] + + self.chunked_read = False + + if te: + for enc in te: + if enc == "chunked": + self.chunked_read = True + else: + # Note that, even if we see "chunked", we must reject + # if there is an extension we don't recognize. + self.simple_response("501 Unimplemented") + self.close_connection = True + return + + # From PEP 333: + # "Servers and gateways that implement HTTP 1.1 must provide + # transparent support for HTTP 1.1's "expect/continue" mechanism. + # This may be done in any of several ways: + # 1. Respond to requests containing an Expect: 100-continue request + # with an immediate "100 Continue" response, and proceed normally. + # 2. Proceed with the request normally, but provide the application + # with a wsgi.input stream that will send the "100 Continue" + # response if/when the application first attempts to read from + # the input stream. The read request must then remain blocked + # until the client responds. + # 3. Wait until the client decides that the server does not support + # expect/continue, and sends the request body on its own. + # (This is suboptimal, and is not recommended.) + # + # We used to do 3, but are now doing 1. Maybe we'll do 2 someday, + # but it seems like it would be a big slowdown for such a rare case. + if environ.get("HTTP_EXPECT", "") == "100-continue": + self.simple_response(100) + + self.ready = True + + def read_headers(self): + """Read header lines from the incoming stream.""" + environ = self.environ + + while True: + line = self.rfile.readline() + if not line: + # No more data--illegal end of headers + raise ValueError("Illegal end of headers.") + + if line == '\r\n': + # Normal end of headers + break + + if line[0] in ' \t': + # It's a continuation line. + v = line.strip() + else: + k, v = line.split(":", 1) + k, v = k.strip().upper(), v.strip() + envname = "HTTP_" + k.replace("-", "_") + + if k in comma_separated_headers: + existing = environ.get(envname) + if existing: + v = ", ".join((existing, v)) + environ[envname] = v + + ct = environ.pop("HTTP_CONTENT_TYPE", None) + if ct is not None: + environ["CONTENT_TYPE"] = ct + cl = environ.pop("HTTP_CONTENT_LENGTH", None) + if cl is not None: + environ["CONTENT_LENGTH"] = cl + + def decode_chunked(self): + """Decode the 'chunked' transfer coding.""" + cl = 0 + data = StringIO.StringIO() + while True: + line = self.rfile.readline().strip().split(";", 1) + chunk_size = int(line.pop(0), 16) + if chunk_size <= 0: + break +## if line: chunk_extension = line[0] + cl += chunk_size + data.write(self.rfile.read(chunk_size)) + crlf = self.rfile.read(2) + if crlf != "\r\n": + self.simple_response("400 Bad Request", + "Bad chunked transfer coding " + "(expected '\\r\\n', got %r)" % crlf) + return + + # Grab any trailer headers + self.read_headers() + + data.seek(0) + self.environ["wsgi.input"] = data + self.environ["CONTENT_LENGTH"] = str(cl) or "" + return True + + def respond(self): + """Call the appropriate WSGI app and write its iterable output.""" + # Set rfile.maxlen to ensure we don't read past Content-Length. + # This will also be used to read the entire request body if errors + # are raised before the app can read the body. + if self.chunked_read: + # If chunked, Content-Length will be 0. + self.rfile.maxlen = self.max_request_body_size + else: + cl = int(self.environ.get("CONTENT_LENGTH", 0)) + if self.max_request_body_size: + self.rfile.maxlen = min(cl, self.max_request_body_size) + else: + self.rfile.maxlen = cl + self.rfile.bytes_read = 0 + + try: + self._respond() + except MaxSizeExceeded: + if not self.sent_headers: + self.simple_response("413 Request Entity Too Large") + return + + def _respond(self): + if self.chunked_read: + if not self.decode_chunked(): + self.close_connection = True + return + + response = self.wsgi_app(self.environ, self.start_response) + try: + for chunk in response: + # "The start_response callable must not actually transmit + # the response headers. Instead, it must store them for the + # server or gateway to transmit only after the first + # iteration of the application return value that yields + # a NON-EMPTY string, or upon the application's first + # invocation of the write() callable." (PEP 333) + if chunk: + self.write(chunk) + finally: + if hasattr(response, "close"): + response.close() + + if (self.ready and not self.sent_headers): + self.sent_headers = True + self.send_headers() + if self.chunked_write: + self.wfile.sendall("0\r\n\r\n") + + def simple_response(self, status, msg=""): + """Write a simple response back to the client.""" + status = str(status) + buf = ["%s %s\r\n" % (self.environ['ACTUAL_SERVER_PROTOCOL'], status), + "Content-Length: %s\r\n" % len(msg), + "Content-Type: text/plain\r\n"] + + if status[:3] == "413" and self.response_protocol == 'HTTP/1.1': + # Request Entity Too Large + self.close_connection = True + buf.append("Connection: close\r\n") + + buf.append("\r\n") + if msg: + buf.append(msg) + + try: + self.wfile.sendall("".join(buf)) + except socket.error as x: + if x.args[0] not in socket_errors_to_ignore: + raise + + def start_response(self, status, headers, exc_info = None): + """WSGI callable to begin the HTTP response.""" + # "The application may call start_response more than once, + # if and only if the exc_info argument is provided." + if self.started_response and not exc_info: + raise AssertionError("WSGI start_response called a second " + "time with no exc_info.") + + # "if exc_info is provided, and the HTTP headers have already been + # sent, start_response must raise an error, and should raise the + # exc_info tuple." + if self.sent_headers: + try: + raise exc_info[1].with_traceback(exc_info[2]) + finally: + exc_info = None + + self.started_response = True + self.status = status + self.outheaders.extend(headers) + return self.write + + def write(self, chunk): + """WSGI callable to write unbuffered data to the client. + + This method is also used internally by start_response (to write + data from the iterable returned by the WSGI application). + """ + if not self.started_response: + raise AssertionError("WSGI write called before start_response.") + + if not self.sent_headers: + self.sent_headers = True + self.send_headers() + + if self.chunked_write and chunk: + buf = [hex(len(chunk))[2:], "\r\n", chunk, "\r\n"] + self.wfile.sendall("".join(buf)) + else: + self.wfile.sendall(chunk) + + def send_headers(self): + """Assert, process, and send the HTTP response message-headers.""" + hkeys = [key.lower() for key, value in self.outheaders] + status = int(self.status[:3]) + + if status == 413: + # Request Entity Too Large. Close conn to avoid garbage. + self.close_connection = True + elif "content-length" not in hkeys: + # "All 1xx (informational), 204 (no content), + # and 304 (not modified) responses MUST NOT + # include a message-body." So no point chunking. + if status < 200 or status in (204, 205, 304): + pass + else: + if (self.response_protocol == 'HTTP/1.1' + and self.environ["REQUEST_METHOD"] != 'HEAD'): + # Use the chunked transfer-coding + self.chunked_write = True + self.outheaders.append(("Transfer-Encoding", "chunked")) + else: + # Closing the conn is the only way to determine len. + self.close_connection = True + + if "connection" not in hkeys: + if self.response_protocol == 'HTTP/1.1': + # Both server and client are HTTP/1.1 or better + if self.close_connection: + self.outheaders.append(("Connection", "close")) + else: + # Server and/or client are HTTP/1.0 + if not self.close_connection: + self.outheaders.append(("Connection", "Keep-Alive")) + + if (not self.close_connection) and (not self.chunked_read): + # Read any remaining request body data on the socket. + # "If an origin server receives a request that does not include an + # Expect request-header field with the "100-continue" expectation, + # the request includes a request body, and the server responds + # with a final status code before reading the entire request body + # from the transport connection, then the server SHOULD NOT close + # the transport connection until it has read the entire request, + # or until the client closes the connection. Otherwise, the client + # might not reliably receive the response message. However, this + # requirement is not be construed as preventing a server from + # defending itself against denial-of-service attacks, or from + # badly broken client implementations." + size = self.rfile.maxlen - self.rfile.bytes_read + if size > 0: + self.rfile.read(size) + + if "date" not in hkeys: + self.outheaders.append(("Date", rfc822.formatdate())) + + if "server" not in hkeys: + self.outheaders.append(("Server", self.environ['SERVER_SOFTWARE'])) + + buf = [self.environ['ACTUAL_SERVER_PROTOCOL'], " ", self.status, "\r\n"] + try: + buf += [k + ": " + v + "\r\n" for k, v in self.outheaders] + except TypeError: + if not isinstance(k, str): + raise TypeError("WSGI response header key %r is not a string.") + if not isinstance(v, str): + raise TypeError("WSGI response header value %r is not a string.") + else: + raise + buf.append("\r\n") + self.wfile.sendall("".join(buf)) + + +class NoSSLError(Exception): + """Exception raised when a client speaks HTTP to an HTTPS socket.""" + pass + + +class FatalSSLAlert(Exception): + """Exception raised when the SSL implementation signals a fatal alert.""" + pass + + +if not _fileobject_uses_str_type: + class CP_fileobject(socket._fileobject): + """Faux file object attached to a socket object.""" + + def sendall(self, data): + """Sendall for non-blocking sockets.""" + while data: + try: + bytes_sent = self.send(data) + data = data[bytes_sent:] + except socket.error as e: + if e.args[0] not in socket_errors_nonblocking: + raise + + def send(self, data): + return self._sock.send(data) + + def flush(self): + if self._wbuf: + buffer = "".join(self._wbuf) + self._wbuf = [] + self.sendall(buffer) + + def recv(self, size): + while True: + try: + return self._sock.recv(size) + except socket.error as e: + if (e.args[0] not in socket_errors_nonblocking + and e.args[0] not in socket_error_eintr): + raise + + def read(self, size=-1): + # Use max, disallow tiny reads in a loop as they are very inefficient. + # We never leave read() with any leftover data from a new recv() call + # in our internal buffer. + rbufsize = max(self._rbufsize, self.default_bufsize) + # Our use of StringIO rather than lists of string objects returned by + # recv() minimizes memory usage and fragmentation that occurs when + # rbufsize is large compared to the typical return value of recv(). + buf = self._rbuf + buf.seek(0, 2) # seek end + if size < 0: + # Read until EOF + self._rbuf = StringIO.StringIO() # reset _rbuf. we consume it via buf. + while True: + data = self.recv(rbufsize) + if not data: + break + buf.write(data) + return buf.getvalue() + else: + # Read until size bytes or EOF seen, whichever comes first + buf_len = buf.tell() + if buf_len >= size: + # Already have size bytes in our buffer? Extract and return. + buf.seek(0) + rv = buf.read(size) + self._rbuf = StringIO.StringIO() + self._rbuf.write(buf.read()) + return rv + + self._rbuf = StringIO.StringIO() # reset _rbuf. we consume it via buf. + while True: + left = size - buf_len + # recv() will malloc the amount of memory given as its + # parameter even though it often returns much less data + # than that. The returned data string is short lived + # as we copy it into a StringIO and free it. This avoids + # fragmentation issues on many platforms. + data = self.recv(left) + if not data: + break + n = len(data) + if n == size and not buf_len: + # Shortcut. Avoid buffer data copies when: + # - We have no data in our buffer. + # AND + # - Our call to recv returned exactly the + # number of bytes we were asked to read. + return data + if n == left: + buf.write(data) + del data # explicit free + break + assert n <= left, "recv(%d) returned %d bytes" % (left, n) + buf.write(data) + buf_len += n + del data # explicit free + #assert buf_len == buf.tell() + return buf.getvalue() + + def readline(self, size=-1): + buf = self._rbuf + buf.seek(0, 2) # seek end + if buf.tell() > 0: + # check if we already have it in our buffer + buf.seek(0) + bline = buf.readline(size) + if bline.endswith('\n') or len(bline) == size: + self._rbuf = StringIO.StringIO() + self._rbuf.write(buf.read()) + return bline + del bline + if size < 0: + # Read until \n or EOF, whichever comes first + if self._rbufsize <= 1: + # Speed up unbuffered case + buf.seek(0) + buffers = [buf.read()] + self._rbuf = StringIO.StringIO() # reset _rbuf. we consume it via buf. + data = None + recv = self.recv + while data != "\n": + data = recv(1) + if not data: + break + buffers.append(data) + return "".join(buffers) + + buf.seek(0, 2) # seek end + self._rbuf = StringIO.StringIO() # reset _rbuf. we consume it via buf. + while True: + data = self.recv(self._rbufsize) + if not data: + break + nl = data.find('\n') + if nl >= 0: + nl += 1 + buf.write(data[:nl]) + self._rbuf.write(data[nl:]) + del data + break + buf.write(data) + return buf.getvalue() + else: + # Read until size bytes or \n or EOF seen, whichever comes first + buf.seek(0, 2) # seek end + buf_len = buf.tell() + if buf_len >= size: + buf.seek(0) + rv = buf.read(size) + self._rbuf = StringIO.StringIO() + self._rbuf.write(buf.read()) + return rv + self._rbuf = StringIO.StringIO() # reset _rbuf. we consume it via buf. + while True: + data = self.recv(self._rbufsize) + if not data: + break + left = size - buf_len + # did we just receive a newline? + nl = data.find('\n', 0, left) + if nl >= 0: + nl += 1 + # save the excess data to _rbuf + self._rbuf.write(data[nl:]) + if buf_len: + buf.write(data[:nl]) + break + else: + # Shortcut. Avoid data copy through buf when returning + # a substring of our first recv(). + return data[:nl] + n = len(data) + if n == size and not buf_len: + # Shortcut. Avoid data copy through buf when + # returning exactly all of our first recv(). + return data + if n >= left: + buf.write(data[:left]) + self._rbuf.write(data[left:]) + break + buf.write(data) + buf_len += n + #assert buf_len == buf.tell() + return buf.getvalue() + +else: + class CP_fileobject(socket._fileobject): + """Faux file object attached to a socket object.""" + + def sendall(self, data): + """Sendall for non-blocking sockets.""" + while data: + try: + bytes_sent = self.send(data) + data = data[bytes_sent:] + except socket.error as e: + if e.args[0] not in socket_errors_nonblocking: + raise + + def send(self, data): + return self._sock.send(data) + + def flush(self): + if self._wbuf: + buffer = "".join(self._wbuf) + self._wbuf = [] + self.sendall(buffer) + + def recv(self, size): + while True: + try: + return self._sock.recv(size) + except socket.error as e: + if (e.args[0] not in socket_errors_nonblocking + and e.args[0] not in socket_error_eintr): + raise + + def read(self, size=-1): + if size < 0: + # Read until EOF + buffers = [self._rbuf] + self._rbuf = "" + if self._rbufsize <= 1: + recv_size = self.default_bufsize + else: + recv_size = self._rbufsize + + while True: + data = self.recv(recv_size) + if not data: + break + buffers.append(data) + return "".join(buffers) + else: + # Read until size bytes or EOF seen, whichever comes first + data = self._rbuf + buf_len = len(data) + if buf_len >= size: + self._rbuf = data[size:] + return data[:size] + buffers = [] + if data: + buffers.append(data) + self._rbuf = "" + while True: + left = size - buf_len + recv_size = max(self._rbufsize, left) + data = self.recv(recv_size) + if not data: + break + buffers.append(data) + n = len(data) + if n >= left: + self._rbuf = data[left:] + buffers[-1] = data[:left] + break + buf_len += n + return "".join(buffers) + + def readline(self, size=-1): + data = self._rbuf + if size < 0: + # Read until \n or EOF, whichever comes first + if self._rbufsize <= 1: + # Speed up unbuffered case + assert data == "" + buffers = [] + while data != "\n": + data = self.recv(1) + if not data: + break + buffers.append(data) + return "".join(buffers) + nl = data.find('\n') + if nl >= 0: + nl += 1 + self._rbuf = data[nl:] + return data[:nl] + buffers = [] + if data: + buffers.append(data) + self._rbuf = "" + while True: + data = self.recv(self._rbufsize) + if not data: + break + buffers.append(data) + nl = data.find('\n') + if nl >= 0: + nl += 1 + self._rbuf = data[nl:] + buffers[-1] = data[:nl] + break + return "".join(buffers) + else: + # Read until size bytes or \n or EOF seen, whichever comes first + nl = data.find('\n', 0, size) + if nl >= 0: + nl += 1 + self._rbuf = data[nl:] + return data[:nl] + buf_len = len(data) + if buf_len >= size: + self._rbuf = data[size:] + return data[:size] + buffers = [] + if data: + buffers.append(data) + self._rbuf = "" + while True: + data = self.recv(self._rbufsize) + if not data: + break + buffers.append(data) + left = size - buf_len + nl = data.find('\n', 0, left) + if nl >= 0: + nl += 1 + self._rbuf = data[nl:] + buffers[-1] = data[:nl] + break + n = len(data) + if n >= left: + self._rbuf = data[left:] + buffers[-1] = data[:left] + break + buf_len += n + return "".join(buffers) + + +class SSL_fileobject(CP_fileobject): + """SSL file object attached to a socket object.""" + + ssl_timeout = 3 + ssl_retry = .01 + + def _safe_call(self, is_reader, call, *args, **kwargs): + """Wrap the given call with SSL error-trapping. + + is_reader: if False EOF errors will be raised. If True, EOF errors + will return "" (to emulate normal sockets). + """ + start = time.time() + while True: + try: + return call(*args, **kwargs) + except SSL.WantReadError: + # Sleep and try again. This is dangerous, because it means + # the rest of the stack has no way of differentiating + # between a "new handshake" error and "client dropped". + # Note this isn't an endless loop: there's a timeout below. + time.sleep(self.ssl_retry) + except SSL.WantWriteError: + time.sleep(self.ssl_retry) + except SSL.SysCallError as e: + if is_reader and e.args == (-1, 'Unexpected EOF'): + return "" + + errnum = e.args[0] + if is_reader and errnum in socket_errors_to_ignore: + return "" + raise socket.error(errnum) + except SSL.Error as e: + if is_reader and e.args == (-1, 'Unexpected EOF'): + return "" + + thirdarg = None + try: + thirdarg = e.args[0][0][2] + except IndexError: + pass + + if thirdarg == 'http request': + # The client is talking HTTP to an HTTPS server. + raise NoSSLError() + raise FatalSSLAlert(*e.args) + except: + raise + + if time.time() - start > self.ssl_timeout: + raise socket.timeout("timed out") + + def recv(self, *args, **kwargs): + buf = [] + r = super(SSL_fileobject, self).recv + while True: + data = self._safe_call(True, r, *args, **kwargs) + buf.append(data) + p = self._sock.pending() + if not p: + return "".join(buf) + + def sendall(self, *args, **kwargs): + return self._safe_call(False, super(SSL_fileobject, self).sendall, *args, **kwargs) + + def send(self, *args, **kwargs): + return self._safe_call(False, super(SSL_fileobject, self).send, *args, **kwargs) + + +class HTTPConnection(object): + """An HTTP connection (active socket). + + socket: the raw socket object (usually TCP) for this connection. + wsgi_app: the WSGI application for this server/connection. + environ: a WSGI environ template. This will be copied for each request. + + rfile: a fileobject for reading from the socket. + send: a function for writing (+ flush) to the socket. + """ + + rbufsize = -1 + RequestHandlerClass = HTTPRequest + environ = {"wsgi.version": (1, 0), + "wsgi.url_scheme": "http", + "wsgi.multithread": True, + "wsgi.multiprocess": False, + "wsgi.run_once": False, + "wsgi.errors": sys.stderr, + } + + def __init__(self, sock, wsgi_app, environ): + self.socket = sock + self.wsgi_app = wsgi_app + + # Copy the class environ into self. + self.environ = self.environ.copy() + self.environ.update(environ) + + if SSL and isinstance(sock, SSL.ConnectionType): + timeout = sock.gettimeout() + self.rfile = SSL_fileobject(sock, "rb", self.rbufsize) + self.rfile.ssl_timeout = timeout + self.wfile = SSL_fileobject(sock, "wb", -1) + self.wfile.ssl_timeout = timeout + else: + self.rfile = CP_fileobject(sock, "rb", self.rbufsize) + self.wfile = CP_fileobject(sock, "wb", -1) + + # Wrap wsgi.input but not HTTPConnection.rfile itself. + # We're also not setting maxlen yet; we'll do that separately + # for headers and body for each iteration of self.communicate + # (if maxlen is 0 the wrapper doesn't check length). + self.environ["wsgi.input"] = SizeCheckWrapper(self.rfile, 0) + + def communicate(self): + """Read each request and respond appropriately.""" + try: + while True: + # (re)set req to None so that if something goes wrong in + # the RequestHandlerClass constructor, the error doesn't + # get written to the previous request. + req = None + req = self.RequestHandlerClass(self.wfile, self.environ, + self.wsgi_app) + + # This order of operations should guarantee correct pipelining. + req.parse_request() + if not req.ready: + return + + req.respond() + if req.close_connection: + return + + except socket.error as e: + errnum = e.args[0] + if errnum == 'timed out': + if req and not req.sent_headers: + req.simple_response("408 Request Timeout") + elif errnum not in socket_errors_to_ignore: + if req and not req.sent_headers: + req.simple_response("500 Internal Server Error", + format_exc()) + return + except (KeyboardInterrupt, SystemExit): + raise + except FatalSSLAlert as e: + # Close the connection. + return + except NoSSLError: + if req and not req.sent_headers: + # Unwrap our wfile + req.wfile = CP_fileobject(self.socket._sock, "wb", -1) + req.simple_response("400 Bad Request", + "The client sent a plain HTTP request, but " + "this server only speaks HTTPS on this port.") + self.linger = True + except Exception as e: + if req and not req.sent_headers: + req.simple_response("500 Internal Server Error", format_exc()) + + linger = False + + def close(self): + """Close the socket underlying this connection.""" + self.rfile.close() + + if not self.linger: + # Python's socket module does NOT call close on the kernel socket + # when you call socket.close(). We do so manually here because we + # want this server to send a FIN TCP segment immediately. Note this + # must be called *before* calling socket.close(), because the latter + # drops its reference to the kernel socket. + self.socket._sock.close() + self.socket.close() + else: + # On the other hand, sometimes we want to hang around for a bit + # to make sure the client has a chance to read our entire + # response. Skipping the close() calls here delays the FIN + # packet until the socket object is garbage-collected later. + # Someday, perhaps, we'll do the full lingering_close that + # Apache does, but not today. + pass + + +def format_exc(limit=None): + """Like print_exc() but return a string. Backport for Python 2.3.""" + try: + etype, value, tb = sys.exc_info() + return ''.join(traceback.format_exception(etype, value, tb, limit)) + finally: + etype = value = tb = None + + +_SHUTDOWNREQUEST = None + +class WorkerThread(threading.Thread): + """Thread which continuously polls a Queue for Connection objects. + + server: the HTTP Server which spawned this thread, and which owns the + Queue and is placing active connections into it. + ready: a simple flag for the calling server to know when this thread + has begun polling the Queue. + + Due to the timing issues of polling a Queue, a WorkerThread does not + check its own 'ready' flag after it has started. To stop the thread, + it is necessary to stick a _SHUTDOWNREQUEST object onto the Queue + (one for each running WorkerThread). + """ + + conn = None + + def __init__(self, server): + self.ready = False + self.server = server + threading.Thread.__init__(self) + + def run(self): + try: + self.ready = True + while True: + conn = self.server.requests.get() + if conn is _SHUTDOWNREQUEST: + return + + self.conn = conn + try: + conn.communicate() + finally: + conn.close() + self.conn = None + except (KeyboardInterrupt, SystemExit), exc: + self.server.interrupt = exc + + +class ThreadPool(object): + """A Request Queue for the CherryPyWSGIServer which pools threads. + + ThreadPool objects must provide min, get(), put(obj), start() + and stop(timeout) attributes. + """ + + def __init__(self, server, min=10, max=-1): + self.server = server + self.min = min + self.max = max + self._threads = [] + self._queue = Queue.Queue() + self.get = self._queue.get + + def start(self): + """Start the pool of threads.""" + for i in range(self.min): + self._threads.append(WorkerThread(self.server)) + for worker in self._threads: + worker.setName("CP WSGIServer " + worker.getName()) + worker.start() + for worker in self._threads: + while not worker.ready: + time.sleep(.1) + + def _get_idle(self): + """Number of worker threads which are idle. Read-only.""" + return len([t for t in self._threads if t.conn is None]) + idle = property(_get_idle, doc=_get_idle.__doc__) + + def put(self, obj): + self._queue.put(obj) + if obj is _SHUTDOWNREQUEST: + return + + def grow(self, amount): + """Spawn new worker threads (not above self.max).""" + for i in range(amount): + if self.max > 0 and len(self._threads) >= self.max: + break + worker = WorkerThread(self.server) + worker.setName("CP WSGIServer " + worker.getName()) + self._threads.append(worker) + worker.start() + + def shrink(self, amount): + """Kill off worker threads (not below self.min).""" + # Grow/shrink the pool if necessary. + # Remove any dead threads from our list + for t in self._threads: + if not t.isAlive(): + self._threads.remove(t) + amount -= 1 + + if amount > 0: + for i in range(min(amount, len(self._threads) - self.min)): + # Put a number of shutdown requests on the queue equal + # to 'amount'. Once each of those is processed by a worker, + # that worker will terminate and be culled from our list + # in self.put. + self._queue.put(_SHUTDOWNREQUEST) + + def stop(self, timeout=5): + # Must shut down threads here so the code that calls + # this method can know when all threads are stopped. + for worker in self._threads: + self._queue.put(_SHUTDOWNREQUEST) + + # Don't join currentThread (when stop is called inside a request). + current = threading.currentThread() + while self._threads: + worker = self._threads.pop() + if worker is not current and worker.isAlive(): + try: + if timeout is None or timeout < 0: + worker.join() + else: + worker.join(timeout) + if worker.isAlive(): + # We exhausted the timeout. + # Forcibly shut down the socket. + c = worker.conn + if c and not c.rfile.closed: + if SSL and isinstance(c.socket, SSL.ConnectionType): + # pyOpenSSL.socket.shutdown takes no args + c.socket.shutdown() + else: + c.socket.shutdown(socket.SHUT_RD) + worker.join() + except (AssertionError, + # Ignore repeated Ctrl-C. + # See http://www.cherrypy.org/ticket/691. + KeyboardInterrupt), exc1: + pass + + + +class SSLConnection: + """A thread-safe wrapper for an SSL.Connection. + + *args: the arguments to create the wrapped SSL.Connection(*args). + """ + + def __init__(self, *args): + self._ssl_conn = SSL.Connection(*args) + self._lock = threading.RLock() + + for f in ('get_context', 'pending', 'send', 'write', 'recv', 'read', + 'renegotiate', 'bind', 'listen', 'connect', 'accept', + 'setblocking', 'fileno', 'shutdown', 'close', 'get_cipher_list', + 'getpeername', 'getsockname', 'getsockopt', 'setsockopt', + 'makefile', 'get_app_data', 'set_app_data', 'state_string', + 'sock_shutdown', 'get_peer_certificate', 'want_read', + 'want_write', 'set_connect_state', 'set_accept_state', + 'connect_ex', 'sendall', 'settimeout'): + exec("""def %s(self, *args): + self._lock.acquire() + try: + return self._ssl_conn.%s(*args) + finally: + self._lock.release() +""" % (f, f)) + + +try: + import fcntl +except ImportError: + try: + from ctypes import windll, WinError + except ImportError: + def prevent_socket_inheritance(sock): + """Dummy function, since neither fcntl nor ctypes are available.""" + pass + else: + def prevent_socket_inheritance(sock): + """Mark the given socket fd as non-inheritable (Windows).""" + if not windll.kernel32.SetHandleInformation(sock.fileno(), 1, 0): + raise WinError() +else: + def prevent_socket_inheritance(sock): + """Mark the given socket fd as non-inheritable (POSIX).""" + fd = sock.fileno() + old_flags = fcntl.fcntl(fd, fcntl.F_GETFD) + fcntl.fcntl(fd, fcntl.F_SETFD, old_flags | fcntl.FD_CLOEXEC) + + +class CherryPyWSGIServer(object): + """An HTTP server for WSGI. + + bind_addr: The interface on which to listen for connections. + For TCP sockets, a (host, port) tuple. Host values may be any IPv4 + or IPv6 address, or any valid hostname. The string 'localhost' is a + synonym for '127.0.0.1' (or '::1', if your hosts file prefers IPv6). + The string '0.0.0.0' is a special IPv4 entry meaning "any active + interface" (INADDR_ANY), and '::' is the similar IN6ADDR_ANY for + IPv6. The empty string or None are not allowed. + + For UNIX sockets, supply the filename as a string. + wsgi_app: the WSGI 'application callable'; multiple WSGI applications + may be passed as (path_prefix, app) pairs. + numthreads: the number of worker threads to create (default 10). + server_name: the string to set for WSGI's SERVER_NAME environ entry. + Defaults to socket.gethostname(). + max: the maximum number of queued requests (defaults to -1 = no limit). + request_queue_size: the 'backlog' argument to socket.listen(); + specifies the maximum number of queued connections (default 5). + timeout: the timeout in seconds for accepted connections (default 10). + + nodelay: if True (the default since 3.1), sets the TCP_NODELAY socket + option. + + protocol: the version string to write in the Status-Line of all + HTTP responses. For example, "HTTP/1.1" (the default). This + also limits the supported features used in the response. + + + SSL/HTTPS + --------- + The OpenSSL module must be importable for SSL functionality. + You can obtain it from http://pyopenssl.sourceforge.net/ + + ssl_certificate: the filename of the server SSL certificate. + ssl_privatekey: the filename of the server's private key file. + + If either of these is None (both are None by default), this server + will not use SSL. If both are given and are valid, they will be read + on server start and used in the SSL context for the listening socket. + """ + + protocol = "HTTP/1.1" + _bind_addr = "127.0.0.1" + version = "CherryPy/3.1.2" + ready = False + _interrupt = None + + nodelay = True + + ConnectionClass = HTTPConnection + environ = {} + + # Paths to certificate and private key files + ssl_certificate = None + ssl_private_key = None + + def __init__(self, bind_addr, wsgi_app, numthreads=10, server_name=None, + max=-1, request_queue_size=5, timeout=10, shutdown_timeout=5): + self.requests = ThreadPool(self, min=numthreads or 1, max=max) + + if callable(wsgi_app): + # We've been handed a single wsgi_app, in CP-2.1 style. + # Assume it's mounted at "". + self.wsgi_app = wsgi_app + else: + # We've been handed a list of (path_prefix, wsgi_app) tuples, + # so that the server can call different wsgi_apps, and also + # correctly set SCRIPT_NAME. + warnings.warn("The ability to pass multiple apps is deprecated " + "and will be removed in 3.2. You should explicitly " + "include a WSGIPathInfoDispatcher instead.", + DeprecationWarning) + self.wsgi_app = WSGIPathInfoDispatcher(wsgi_app) + + self.bind_addr = bind_addr + if not server_name: + server_name = socket.gethostname() + self.server_name = server_name + self.request_queue_size = request_queue_size + + self.timeout = timeout + self.shutdown_timeout = shutdown_timeout + + def _get_numthreads(self): + return self.requests.min + def _set_numthreads(self, value): + self.requests.min = value + numthreads = property(_get_numthreads, _set_numthreads) + + def __str__(self): + return "%s.%s(%r)" % (self.__module__, self.__class__.__name__, + self.bind_addr) + + def _get_bind_addr(self): + return self._bind_addr + def _set_bind_addr(self, value): + if isinstance(value, tuple) and value[0] in ('', None): + # Despite the socket module docs, using '' does not + # allow AI_PASSIVE to work. Passing None instead + # returns '0.0.0.0' like we want. In other words: + # host AI_PASSIVE result + # '' Y 192.168.x.y + # '' N 192.168.x.y + # None Y 0.0.0.0 + # None N 127.0.0.1 + # But since you can get the same effect with an explicit + # '0.0.0.0', we deny both the empty string and None as values. + raise ValueError("Host values of '' or None are not allowed. " + "Use '0.0.0.0' (IPv4) or '::' (IPv6) instead " + "to listen on all active interfaces.") + self._bind_addr = value + bind_addr = property(_get_bind_addr, _set_bind_addr, + doc="""The interface on which to listen for connections. + + For TCP sockets, a (host, port) tuple. Host values may be any IPv4 + or IPv6 address, or any valid hostname. The string 'localhost' is a + synonym for '127.0.0.1' (or '::1', if your hosts file prefers IPv6). + The string '0.0.0.0' is a special IPv4 entry meaning "any active + interface" (INADDR_ANY), and '::' is the similar IN6ADDR_ANY for + IPv6. The empty string or None are not allowed. + + For UNIX sockets, supply the filename as a string.""") + + def start(self): + """Run the server forever.""" + # We don't have to trap KeyboardInterrupt or SystemExit here, + # because cherrpy.server already does so, calling self.stop() for us. + # If you're using this server with another framework, you should + # trap those exceptions in whatever code block calls start(). + self._interrupt = None + + # Select the appropriate socket + if isinstance(self.bind_addr, str): + # AF_UNIX socket + + # So we can reuse the socket... + try: os.unlink(self.bind_addr) + except: pass + + # So everyone can access the socket... + try: os.chmod(self.bind_addr, 0o777) + except: pass + + info = [(socket.AF_UNIX, socket.SOCK_STREAM, 0, "", self.bind_addr)] + else: + # AF_INET or AF_INET6 socket + # Get the correct address family for our host (allows IPv6 addresses) + host, port = self.bind_addr + try: + info = socket.getaddrinfo(host, port, socket.AF_UNSPEC, + socket.SOCK_STREAM, 0, socket.AI_PASSIVE) + except socket.gaierror: + # Probably a DNS issue. Assume IPv4. + info = [(socket.AF_INET, socket.SOCK_STREAM, 0, "", self.bind_addr)] + + self.socket = None + msg = "No socket could be created" + for res in info: + af, socktype, proto, canonname, sa = res + try: + self.bind(af, socktype, proto) + except socket.error as msg: + if self.socket: + self.socket.close() + self.socket = None + continue + break + if not self.socket: + raise socket.error(msg) + # Timeout so KeyboardInterrupt can be caught on Win32 + self.socket.settimeout(1) + self.socket.listen(self.request_queue_size) + + # Create worker threads + self.requests.start() + + self.ready = True + while self.ready: + self.tick() + if self.interrupt: + while self.interrupt is True: + # Wait for self.stop() to complete. See _set_interrupt. + time.sleep(0.1) + if self.interrupt: + raise self.interrupt + + def bind(self, family, type, proto=0): + """Create (or recreate) the actual socket object.""" + self.socket = socket.socket(family, type, proto) + prevent_socket_inheritance(self.socket) + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + if self.nodelay: + self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + if self.ssl_certificate and self.ssl_private_key: + if SSL is None: + raise ImportError("You must install pyOpenSSL to use HTTPS.") + + # See http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/442473 + ctx = SSL.Context(SSL.SSLv23_METHOD) + ctx.use_privatekey_file(self.ssl_private_key) + ctx.use_certificate_file(self.ssl_certificate) + self.socket = SSLConnection(ctx, self.socket) + self.populate_ssl_environ() + + # If listening on the IPV6 any address ('::' = IN6ADDR_ANY), + # activate dual-stack. See http://www.cherrypy.org/ticket/871. + if (not isinstance(self.bind_addr, str) + and self.bind_addr[0] == '::' and family == socket.AF_INET6): + try: + self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0) + except (AttributeError, socket.error): + # Apparently, the socket option is not available in + # this machine's TCP stack + pass + + self.socket.bind(self.bind_addr) + + def tick(self): + """Accept a new connection and put it on the Queue.""" + try: + s, addr = self.socket.accept() + prevent_socket_inheritance(s) + if not self.ready: + return + if hasattr(s, 'settimeout'): + s.settimeout(self.timeout) + + environ = self.environ.copy() + # SERVER_SOFTWARE is common for IIS. It's also helpful for + # us to pass a default value for the "Server" response header. + if environ.get("SERVER_SOFTWARE") is None: + environ["SERVER_SOFTWARE"] = "%s WSGI Server" % self.version + # set a non-standard environ entry so the WSGI app can know what + # the *real* server protocol is (and what features to support). + # See http://www.faqs.org/rfcs/rfc2145.html. + environ["ACTUAL_SERVER_PROTOCOL"] = self.protocol + environ["SERVER_NAME"] = self.server_name + + if isinstance(self.bind_addr, str): + # AF_UNIX. This isn't really allowed by WSGI, which doesn't + # address unix domain sockets. But it's better than nothing. + environ["SERVER_PORT"] = "" + else: + environ["SERVER_PORT"] = str(self.bind_addr[1]) + # optional values + # Until we do DNS lookups, omit REMOTE_HOST + environ["REMOTE_ADDR"] = addr[0] + environ["REMOTE_PORT"] = str(addr[1]) + + conn = self.ConnectionClass(s, self.wsgi_app, environ) + self.requests.put(conn) + except socket.timeout: + # The only reason for the timeout in start() is so we can + # notice keyboard interrupts on Win32, which don't interrupt + # accept() by default + return + except socket.error as x: + if x.args[0] in socket_error_eintr: + # I *think* this is right. EINTR should occur when a signal + # is received during the accept() call; all docs say retry + # the call, and I *think* I'm reading it right that Python + # will then go ahead and poll for and handle the signal + # elsewhere. See http://www.cherrypy.org/ticket/707. + return + if x.args[0] in socket_errors_nonblocking: + # Just try again. See http://www.cherrypy.org/ticket/479. + return + if x.args[0] in socket_errors_to_ignore: + # Our socket was closed. + # See http://www.cherrypy.org/ticket/686. + return + raise + + def _get_interrupt(self): + return self._interrupt + def _set_interrupt(self, interrupt): + self._interrupt = True + self.stop() + self._interrupt = interrupt + interrupt = property(_get_interrupt, _set_interrupt, + doc="Set this to an Exception instance to " + "interrupt the server.") + + def stop(self): + """Gracefully shutdown a server that is serving forever.""" + self.ready = False + + sock = getattr(self, "socket", None) + if sock: + if not isinstance(self.bind_addr, str): + # Touch our own socket to make accept() return immediately. + try: + host, port = sock.getsockname()[:2] + except socket.error as x: + if x.args[0] not in socket_errors_to_ignore: + raise + else: + # Note that we're explicitly NOT using AI_PASSIVE, + # here, because we want an actual IP to touch. + # localhost won't work if we've bound to a public IP, + # but it will if we bound to '0.0.0.0' (INADDR_ANY). + for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC, + socket.SOCK_STREAM): + af, socktype, proto, canonname, sa = res + s = None + try: + s = socket.socket(af, socktype, proto) + # See http://groups.google.com/group/cherrypy-users/ + # browse_frm/thread/bbfe5eb39c904fe0 + s.settimeout(1.0) + s.connect((host, port)) + s.close() + except socket.error: + if s: + s.close() + if hasattr(sock, "close"): + sock.close() + self.socket = None + + self.requests.stop(self.shutdown_timeout) + + def populate_ssl_environ(self): + """Create WSGI environ entries to be merged into each request.""" + cert = open(self.ssl_certificate, 'rb').read() + cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert) + ssl_environ = { + "wsgi.url_scheme": "https", + "HTTPS": "on", + # pyOpenSSL doesn't provide access to any of these AFAICT +## 'SSL_PROTOCOL': 'SSLv2', +## SSL_CIPHER string The cipher specification name +## SSL_VERSION_INTERFACE string The mod_ssl program version +## SSL_VERSION_LIBRARY string The OpenSSL program version + } + + # Server certificate attributes + ssl_environ.update({ + 'SSL_SERVER_M_VERSION': cert.get_version(), + 'SSL_SERVER_M_SERIAL': cert.get_serial_number(), +## 'SSL_SERVER_V_START': Validity of server's certificate (start time), +## 'SSL_SERVER_V_END': Validity of server's certificate (end time), + }) + + for prefix, dn in [("I", cert.get_issuer()), + ("S", cert.get_subject())]: + # X509Name objects don't seem to have a way to get the + # complete DN string. Use str() and slice it instead, + # because str(dn) == "" + dnstr = str(dn)[18:-2] + + wsgikey = 'SSL_SERVER_%s_DN' % prefix + ssl_environ[wsgikey] = dnstr + + # The DN should be of the form: /k1=v1/k2=v2, but we must allow + # for any value to contain slashes itself (in a URL). + while dnstr: + pos = dnstr.rfind("=") + dnstr, value = dnstr[:pos], dnstr[pos + 1:] + pos = dnstr.rfind("/") + dnstr, key = dnstr[:pos], dnstr[pos + 1:] + if key and value: + wsgikey = 'SSL_SERVER_%s_DN_%s' % (prefix, key) + ssl_environ[wsgikey] = value + + self.environ.update(ssl_environ) + diff --git a/thirdparty/origapy/origapy.py b/thirdparty/origapy/origapy.py index 67abca8..0410b57 100644 --- a/thirdparty/origapy/origapy.py +++ b/thirdparty/origapy/origapy.py @@ -97,13 +97,14 @@ def launch(self): # launch Ruby script with popen: # (using the ruby script path as working directory) self.pdfclean = Popen(['ruby', self.pdfclean_script], - stdin=PIPE, stdout=PIPE, stderr=STDOUT, cwd=self.pdfclean_path) + stdin=PIPE, stdout=PIPE, stderr=STDOUT, cwd=self.pdfclean_path, + text=True) ## # go back to original dir: ## os.chdir(original_dir) # check if pdfclean is alive: if self.pdfclean.poll() is not None: self.log.debug('slave has terminated.') - raise RuntimeError, 'Error while launching %s' % self.pdfclean_path + raise RuntimeError('Error while launching %s' % self.pdfclean_path) self._launched = True @@ -158,7 +159,7 @@ def clean (self, input_file, output_file): # check if an error occured: if error or FLAG_ERROR in result: self.log.debug('*** ERROR ***') - raise RuntimeError, 'Error while parsing PDF content' + raise RuntimeError('Error while parsing PDF content') # check if active content was found and cleaned or not: if FLAG_CLEANED in result: self.log.debug('Active content was found and cleaned.') @@ -181,24 +182,19 @@ def stop (self): #=== MAIN ===================================================================== if __name__ == '__main__': - print __doc__ - print '' + print(__doc__) + print('') # console logger with debug level: logging.basicConfig(level=logging.DEBUG, format='%(levelname)-8s %(message)s') - # hack to change default encoding to Latin-1 instead of ASCII: - import sys - reload(sys) - sys.setdefaultencoding( 'latin-1' ) - pdfcleaner = PDF_Cleaner() while True: # read user input, expression to be evaluated: - infile = raw_input('Enter input PDF filename or exit:') + infile = input('Enter input PDF filename or exit:') if infile == 'exit': break - outfile = raw_input('Enter output PDF filename or exit:') + outfile = input('Enter output PDF filename or exit:') pdfcleaner.clean(infile, outfile) diff --git a/thirdparty/path/path.py b/thirdparty/path/path.py index 1604d4a..4f62d1a 100644 --- a/thirdparty/path/path.py +++ b/thirdparty/path/path.py @@ -1,973 +1,959 @@ -""" path.py - An object representing a path to a file or directory. - -Example: - -from path import path -d = path('/home/guido/bin') -for f in d.files('*.py'): - f.chmod(0755) - -This module requires Python 2.2 or later. - - -URL: http://www.jorendorff.com/articles/python/path -Author: Jason Orendorff (and others - see the url!) -Date: 9 Mar 2007 -""" - - -# TODO -# - Tree-walking functions don't avoid symlink loops. Matt Harrison -# sent me a patch for this. -# - Bug in write_text(). It doesn't support Universal newline mode. -# - Better error message in listdir() when self isn't a -# directory. (On Windows, the error message really sucks.) -# - Make sure everything has a good docstring. -# - Add methods for regex find and replace. -# - guess_content_type() method? -# - Perhaps support arguments to touch(). - -from __future__ import generators - -# hack P. Lagadec 2009-11-11: removed md5 import to avoid DeprecationWarning with Python 2.6 -# BUT this breaks read_md5()... -#TODO: use hashlib instead when available (Python 2.5+), else fallback to md5 -import sys, warnings, os, fnmatch, glob, shutil, codecs#, md5 - -__version__ = '2.2' -__all__ = ['path'] - -# Platform-specific support for path.owner -if os.name == 'nt': - try: - import win32security - except ImportError: - win32security = None -else: - try: - import pwd - except ImportError: - pwd = None - -# Pre-2.3 support. Are unicode filenames supported? -_base = str -_getcwd = os.getcwd -try: - if os.path.supports_unicode_filenames: - _base = unicode - _getcwd = os.getcwdu -except AttributeError: - pass - -# Pre-2.3 workaround for booleans -try: - True, False -except NameError: - True, False = 1, 0 - -# Pre-2.3 workaround for basestring. -try: - basestring -except NameError: - basestring = (str, unicode) - -# Universal newline support -_textmode = 'r' -if hasattr(file, 'newlines'): - _textmode = 'U' - - -class TreeWalkWarning(Warning): - pass - -class path(_base): - """ Represents a filesystem path. - - For documentation on individual methods, consult their - counterparts in os.path. - """ - - # --- Special Python methods. - - def __repr__(self): - return 'path(%s)' % _base.__repr__(self) - - # Adding a path and a string yields a path. - def __add__(self, more): - try: - resultStr = _base.__add__(self, more) - except TypeError: #Python bug - resultStr = NotImplemented - if resultStr is NotImplemented: - return resultStr - return self.__class__(resultStr) - - def __radd__(self, other): - if isinstance(other, basestring): - return self.__class__(other.__add__(self)) - else: - return NotImplemented - - # The / operator joins paths. - def __div__(self, rel): - """ fp.__div__(rel) == fp / rel == fp.joinpath(rel) - - Join two path components, adding a separator character if - needed. - """ - return self.__class__(os.path.join(self, rel)) - - # Make the / operator work even when true division is enabled. - __truediv__ = __div__ - - def getcwd(cls): - """ Return the current working directory as a path object. """ - return cls(_getcwd()) - getcwd = classmethod(getcwd) - - - # --- Operations on path strings. - - isabs = os.path.isabs - def abspath(self): return self.__class__(os.path.abspath(self)) - def normcase(self): return self.__class__(os.path.normcase(self)) - def normpath(self): return self.__class__(os.path.normpath(self)) - def realpath(self): return self.__class__(os.path.realpath(self)) - def expanduser(self): return self.__class__(os.path.expanduser(self)) - def expandvars(self): return self.__class__(os.path.expandvars(self)) - def dirname(self): return self.__class__(os.path.dirname(self)) - basename = os.path.basename - - def expand(self): - """ Clean up a filename by calling expandvars(), - expanduser(), and normpath() on it. - - This is commonly everything needed to clean up a filename - read from a configuration file, for example. - """ - return self.expandvars().expanduser().normpath() - - def _get_namebase(self): - base, ext = os.path.splitext(self.name) - return base - - def _get_ext(self): - f, ext = os.path.splitext(_base(self)) - return ext - - def _get_drive(self): - drive, r = os.path.splitdrive(self) - return self.__class__(drive) - - parent = property( - dirname, None, None, - """ This path's parent directory, as a new path object. - - For example, path('/usr/local/lib/libpython.so').parent == path('/usr/local/lib') - """) - - name = property( - basename, None, None, - """ The name of this file or directory without the full path. - - For example, path('/usr/local/lib/libpython.so').name == 'libpython.so' - """) - - namebase = property( - _get_namebase, None, None, - """ The same as path.name, but with one file extension stripped off. - - For example, path('/home/guido/python.tar.gz').name == 'python.tar.gz', - but path('/home/guido/python.tar.gz').namebase == 'python.tar' - """) - - ext = property( - _get_ext, None, None, - """ The file extension, for example '.py'. """) - - drive = property( - _get_drive, None, None, - """ The drive specifier, for example 'C:'. - This is always empty on systems that don't use drive specifiers. - """) - - def splitpath(self): - """ p.splitpath() -> Return (p.parent, p.name). """ - parent, child = os.path.split(self) - return self.__class__(parent), child - - def splitdrive(self): - """ p.splitdrive() -> Return (p.drive, ). - - Split the drive specifier from this path. If there is - no drive specifier, p.drive is empty, so the return value - is simply (path(''), p). This is always the case on Unix. - """ - drive, rel = os.path.splitdrive(self) - return self.__class__(drive), rel - - def splitext(self): - """ p.splitext() -> Return (p.stripext(), p.ext). - - Split the filename extension from this path and return - the two parts. Either part may be empty. - - The extension is everything from '.' to the end of the - last path segment. This has the property that if - (a, b) == p.splitext(), then a + b == p. - """ - filename, ext = os.path.splitext(self) - return self.__class__(filename), ext - - def stripext(self): - """ p.stripext() -> Remove one file extension from the path. - - For example, path('/home/guido/python.tar.gz').stripext() - returns path('/home/guido/python.tar'). - """ - return self.splitext()[0] - - if hasattr(os.path, 'splitunc'): - def splitunc(self): - unc, rest = os.path.splitunc(self) - return self.__class__(unc), rest - - def _get_uncshare(self): - unc, r = os.path.splitunc(self) - return self.__class__(unc) - - uncshare = property( - _get_uncshare, None, None, - """ The UNC mount point for this path. - This is empty for paths on local drives. """) - - def joinpath(self, *args): - """ Join two or more path components, adding a separator - character (os.sep) if needed. Returns a new path - object. - """ - return self.__class__(os.path.join(self, *args)) - - def splitall(self): - r""" Return a list of the path components in this path. - - The first item in the list will be a path. Its value will be - either os.curdir, os.pardir, empty, or the root directory of - this path (for example, '/' or 'C:\\'). The other items in - the list will be strings. - - path.path.joinpath(*result) will yield the original path. - """ - parts = [] - loc = self - while loc != os.curdir and loc != os.pardir: - prev = loc - loc, child = prev.splitpath() - if loc == prev: - break - parts.append(child) - parts.append(loc) - parts.reverse() - return parts - - def relpath(self): - """ Return this path as a relative path, - based from the current working directory. - """ - cwd = self.__class__(os.getcwd()) - return cwd.relpathto(self) - - def relpathto(self, dest): - """ Return a relative path from self to dest. - - If there is no relative path from self to dest, for example if - they reside on different drives in Windows, then this returns - dest.abspath(). - """ - origin = self.abspath() - dest = self.__class__(dest).abspath() - - orig_list = origin.normcase().splitall() - # Don't normcase dest! We want to preserve the case. - dest_list = dest.splitall() - - if orig_list[0] != os.path.normcase(dest_list[0]): - # Can't get here from there. - return dest - - # Find the location where the two paths start to differ. - i = 0 - for start_seg, dest_seg in zip(orig_list, dest_list): - if start_seg != os.path.normcase(dest_seg): - break - i += 1 - - # Now i is the point where the two paths diverge. - # Need a certain number of "os.pardir"s to work up - # from the origin to the point of divergence. - segments = [os.pardir] * (len(orig_list) - i) - # Need to add the diverging part of dest_list. - segments += dest_list[i:] - if len(segments) == 0: - # If they happen to be identical, use os.curdir. - relpath = os.curdir - else: - relpath = os.path.join(*segments) - return self.__class__(relpath) - - # --- Listing, searching, walking, and matching - - def listdir(self, pattern=None): - """ D.listdir() -> List of items in this directory. - - Use D.files() or D.dirs() instead if you want a listing - of just files or just subdirectories. - - The elements of the list are path objects. - - With the optional 'pattern' argument, this only lists - items whose names match the given pattern. - """ - names = os.listdir(self) - if pattern is not None: - names = fnmatch.filter(names, pattern) - return [self / child for child in names] - - def dirs(self, pattern=None): - """ D.dirs() -> List of this directory's subdirectories. - - The elements of the list are path objects. - This does not walk recursively into subdirectories - (but see path.walkdirs). - - With the optional 'pattern' argument, this only lists - directories whose names match the given pattern. For - example, d.dirs('build-*'). - """ - return [p for p in self.listdir(pattern) if p.isdir()] - - def files(self, pattern=None): - """ D.files() -> List of the files in this directory. - - The elements of the list are path objects. - This does not walk into subdirectories (see path.walkfiles). - - With the optional 'pattern' argument, this only lists files - whose names match the given pattern. For example, - d.files('*.pyc'). - """ - - return [p for p in self.listdir(pattern) if p.isfile()] - - def walk(self, pattern=None, errors='strict'): - """ D.walk() -> iterator over files and subdirs, recursively. - - The iterator yields path objects naming each child item of - this directory and its descendants. This requires that - D.isdir(). - - This performs a depth-first traversal of the directory tree. - Each directory is returned just before all its children. - - The errors= keyword argument controls behavior when an - error occurs. The default is 'strict', which causes an - exception. The other allowed values are 'warn', which - reports the error via warnings.warn(), and 'ignore'. - """ - if errors not in ('strict', 'warn', 'ignore'): - raise ValueError("invalid errors parameter") - - try: - childList = self.listdir() - except Exception: - if errors == 'ignore': - return - elif errors == 'warn': - warnings.warn( - "Unable to list directory '%s': %s" - % (self, sys.exc_info()[1]), - TreeWalkWarning) - return - else: - raise - - for child in childList: - if pattern is None or child.fnmatch(pattern): - yield child - try: - isdir = child.isdir() - except Exception: - if errors == 'ignore': - isdir = False - elif errors == 'warn': - warnings.warn( - "Unable to access '%s': %s" - % (child, sys.exc_info()[1]), - TreeWalkWarning) - isdir = False - else: - raise - - if isdir: - for item in child.walk(pattern, errors): - yield item - - def walkdirs(self, pattern=None, errors='strict'): - """ D.walkdirs() -> iterator over subdirs, recursively. - - With the optional 'pattern' argument, this yields only - directories whose names match the given pattern. For - example, mydir.walkdirs('*test') yields only directories - with names ending in 'test'. - - The errors= keyword argument controls behavior when an - error occurs. The default is 'strict', which causes an - exception. The other allowed values are 'warn', which - reports the error via warnings.warn(), and 'ignore'. - """ - if errors not in ('strict', 'warn', 'ignore'): - raise ValueError("invalid errors parameter") - - try: - dirs = self.dirs() - except Exception: - if errors == 'ignore': - return - elif errors == 'warn': - warnings.warn( - "Unable to list directory '%s': %s" - % (self, sys.exc_info()[1]), - TreeWalkWarning) - return - else: - raise - - for child in dirs: - if pattern is None or child.fnmatch(pattern): - yield child - for subsubdir in child.walkdirs(pattern, errors): - yield subsubdir - - def walkfiles(self, pattern=None, errors='strict'): - """ D.walkfiles() -> iterator over files in D, recursively. - - The optional argument, pattern, limits the results to files - with names that match the pattern. For example, - mydir.walkfiles('*.tmp') yields only files with the .tmp - extension. - """ - if errors not in ('strict', 'warn', 'ignore'): - raise ValueError("invalid errors parameter") - - try: - childList = self.listdir() - except Exception: - if errors == 'ignore': - return - elif errors == 'warn': - warnings.warn( - "Unable to list directory '%s': %s" - % (self, sys.exc_info()[1]), - TreeWalkWarning) - return - else: - raise - - for child in childList: - try: - isfile = child.isfile() - isdir = not isfile and child.isdir() - except: - if errors == 'ignore': - continue - elif errors == 'warn': - warnings.warn( - "Unable to access '%s': %s" - % (self, sys.exc_info()[1]), - TreeWalkWarning) - continue - else: - raise - - if isfile: - if pattern is None or child.fnmatch(pattern): - yield child - elif isdir: - for f in child.walkfiles(pattern, errors): - yield f - - def fnmatch(self, pattern): - """ Return True if self.name matches the given pattern. - - pattern - A filename pattern with wildcards, - for example '*.py'. - """ - return fnmatch.fnmatch(self.name, pattern) - - def glob(self, pattern): - """ Return a list of path objects that match the pattern. - - pattern - a path relative to this directory, with wildcards. - - For example, path('/users').glob('*/bin/*') returns a list - of all the files users have in their bin directories. - """ - cls = self.__class__ - return [cls(s) for s in glob.glob(_base(self / pattern))] - - - # --- Reading or writing an entire file at once. - - def open(self, mode='r'): - """ Open this file. Return a file object. """ - return file(self, mode) - - def bytes(self): - """ Open this file, read all bytes, return them as a string. """ - f = self.open('rb') - try: - return f.read() - finally: - f.close() - - def write_bytes(self, bytes, append=False): - """ Open this file and write the given bytes to it. - - Default behavior is to overwrite any existing file. - Call p.write_bytes(bytes, append=True) to append instead. - """ - if append: - mode = 'ab' - else: - mode = 'wb' - f = self.open(mode) - try: - f.write(bytes) - finally: - f.close() - - def text(self, encoding=None, errors='strict'): - r""" Open this file, read it in, return the content as a string. - - This uses 'U' mode in Python 2.3 and later, so '\r\n' and '\r' - are automatically translated to '\n'. - - Optional arguments: - - encoding - The Unicode encoding (or character set) of - the file. If present, the content of the file is - decoded and returned as a unicode object; otherwise - it is returned as an 8-bit str. - errors - How to handle Unicode errors; see help(str.decode) - for the options. Default is 'strict'. - """ - if encoding is None: - # 8-bit - f = self.open(_textmode) - try: - return f.read() - finally: - f.close() - else: - # Unicode - f = codecs.open(self, 'r', encoding, errors) - # (Note - Can't use 'U' mode here, since codecs.open - # doesn't support 'U' mode, even in Python 2.3.) - try: - t = f.read() - finally: - f.close() - return (t.replace(u'\r\n', u'\n') - .replace(u'\r\x85', u'\n') - .replace(u'\r', u'\n') - .replace(u'\x85', u'\n') - .replace(u'\u2028', u'\n')) - - def write_text(self, text, encoding=None, errors='strict', linesep=os.linesep, append=False): - r""" Write the given text to this file. - - The default behavior is to overwrite any existing file; - to append instead, use the 'append=True' keyword argument. - - There are two differences between path.write_text() and - path.write_bytes(): newline handling and Unicode handling. - See below. - - Parameters: - - - text - str/unicode - The text to be written. - - - encoding - str - The Unicode encoding that will be used. - This is ignored if 'text' isn't a Unicode string. - - - errors - str - How to handle Unicode encoding errors. - Default is 'strict'. See help(unicode.encode) for the - options. This is ignored if 'text' isn't a Unicode - string. - - - linesep - keyword argument - str/unicode - The sequence of - characters to be used to mark end-of-line. The default is - os.linesep. You can also specify None; this means to - leave all newlines as they are in 'text'. - - - append - keyword argument - bool - Specifies what to do if - the file already exists (True: append to the end of it; - False: overwrite it.) The default is False. - - - --- Newline handling. - - write_text() converts all standard end-of-line sequences - ('\n', '\r', and '\r\n') to your platform's default end-of-line - sequence (see os.linesep; on Windows, for example, the - end-of-line marker is '\r\n'). - - If you don't like your platform's default, you can override it - using the 'linesep=' keyword argument. If you specifically want - write_text() to preserve the newlines as-is, use 'linesep=None'. - - This applies to Unicode text the same as to 8-bit text, except - there are three additional standard Unicode end-of-line sequences: - u'\x85', u'\r\x85', and u'\u2028'. - - (This is slightly different from when you open a file for - writing with fopen(filename, "w") in C or file(filename, 'w') - in Python.) - - - --- Unicode - - If 'text' isn't Unicode, then apart from newline handling, the - bytes are written verbatim to the file. The 'encoding' and - 'errors' arguments are not used and must be omitted. - - If 'text' is Unicode, it is first converted to bytes using the - specified 'encoding' (or the default encoding if 'encoding' - isn't specified). The 'errors' argument applies only to this - conversion. - - """ - if isinstance(text, unicode): - if linesep is not None: - # Convert all standard end-of-line sequences to - # ordinary newline characters. - text = (text.replace(u'\r\n', u'\n') - .replace(u'\r\x85', u'\n') - .replace(u'\r', u'\n') - .replace(u'\x85', u'\n') - .replace(u'\u2028', u'\n')) - text = text.replace(u'\n', linesep) - if encoding is None: - encoding = sys.getdefaultencoding() - bytes = text.encode(encoding, errors) - else: - # It is an error to specify an encoding if 'text' is - # an 8-bit string. - assert encoding is None - - if linesep is not None: - text = (text.replace('\r\n', '\n') - .replace('\r', '\n')) - bytes = text.replace('\n', linesep) - - self.write_bytes(bytes, append) - - def lines(self, encoding=None, errors='strict', retain=True): - r""" Open this file, read all lines, return them in a list. - - Optional arguments: - encoding - The Unicode encoding (or character set) of - the file. The default is None, meaning the content - of the file is read as 8-bit characters and returned - as a list of (non-Unicode) str objects. - errors - How to handle Unicode errors; see help(str.decode) - for the options. Default is 'strict' - retain - If true, retain newline characters; but all newline - character combinations ('\r', '\n', '\r\n') are - translated to '\n'. If false, newline characters are - stripped off. Default is True. - - This uses 'U' mode in Python 2.3 and later. - """ - if encoding is None and retain: - f = self.open(_textmode) - try: - return f.readlines() - finally: - f.close() - else: - return self.text(encoding, errors).splitlines(retain) - - def write_lines(self, lines, encoding=None, errors='strict', - linesep=os.linesep, append=False): - r""" Write the given lines of text to this file. - - By default this overwrites any existing file at this path. - - This puts a platform-specific newline sequence on every line. - See 'linesep' below. - - lines - A list of strings. - - encoding - A Unicode encoding to use. This applies only if - 'lines' contains any Unicode strings. - - errors - How to handle errors in Unicode encoding. This - also applies only to Unicode strings. - - linesep - The desired line-ending. This line-ending is - applied to every line. If a line already has any - standard line ending ('\r', '\n', '\r\n', u'\x85', - u'\r\x85', u'\u2028'), that will be stripped off and - this will be used instead. The default is os.linesep, - which is platform-dependent ('\r\n' on Windows, '\n' on - Unix, etc.) Specify None to write the lines as-is, - like file.writelines(). - - Use the keyword argument append=True to append lines to the - file. The default is to overwrite the file. Warning: - When you use this with Unicode data, if the encoding of the - existing data in the file is different from the encoding - you specify with the encoding= parameter, the result is - mixed-encoding data, which can really confuse someone trying - to read the file later. - """ - if append: - mode = 'ab' - else: - mode = 'wb' - f = self.open(mode) - try: - for line in lines: - isUnicode = isinstance(line, unicode) - if linesep is not None: - # Strip off any existing line-end and add the - # specified linesep string. - if isUnicode: - if line[-2:] in (u'\r\n', u'\x0d\x85'): - line = line[:-2] - elif line[-1:] in (u'\r', u'\n', - u'\x85', u'\u2028'): - line = line[:-1] - else: - if line[-2:] == '\r\n': - line = line[:-2] - elif line[-1:] in ('\r', '\n'): - line = line[:-1] - line += linesep - if isUnicode: - if encoding is None: - encoding = sys.getdefaultencoding() - line = line.encode(encoding, errors) - f.write(line) - finally: - f.close() - - def read_md5(self): - """ Calculate the md5 hash for this file. - - This reads through the entire file. - """ - f = self.open('rb') - try: - m = md5.new() - while True: - d = f.read(8192) - if not d: - break - m.update(d) - finally: - f.close() - return m.digest() - - # --- Methods for querying the filesystem. - - exists = os.path.exists - isdir = os.path.isdir - isfile = os.path.isfile - islink = os.path.islink - ismount = os.path.ismount - - if hasattr(os.path, 'samefile'): - samefile = os.path.samefile - - getatime = os.path.getatime - atime = property( - getatime, None, None, - """ Last access time of the file. """) - - getmtime = os.path.getmtime - mtime = property( - getmtime, None, None, - """ Last-modified time of the file. """) - - if hasattr(os.path, 'getctime'): - getctime = os.path.getctime - ctime = property( - getctime, None, None, - """ Creation time of the file. """) - - getsize = os.path.getsize - size = property( - getsize, None, None, - """ Size of the file, in bytes. """) - - if hasattr(os, 'access'): - def access(self, mode): - """ Return true if current user has access to this path. - - mode - One of the constants os.F_OK, os.R_OK, os.W_OK, os.X_OK - """ - return os.access(self, mode) - - def stat(self): - """ Perform a stat() system call on this path. """ - return os.stat(self) - - def lstat(self): - """ Like path.stat(), but do not follow symbolic links. """ - return os.lstat(self) - - def get_owner(self): - r""" Return the name of the owner of this file or directory. - - This follows symbolic links. - - On Windows, this returns a name of the form ur'DOMAIN\User Name'. - On Windows, a group can own a file or directory. - """ - if os.name == 'nt': - if win32security is None: - raise Exception("path.owner requires win32all to be installed") - desc = win32security.GetFileSecurity( - self, win32security.OWNER_SECURITY_INFORMATION) - sid = desc.GetSecurityDescriptorOwner() - account, domain, typecode = win32security.LookupAccountSid(None, sid) - return domain + u'\\' + account - else: - if pwd is None: - raise NotImplementedError("path.owner is not implemented on this platform.") - st = self.stat() - return pwd.getpwuid(st.st_uid).pw_name - - owner = property( - get_owner, None, None, - """ Name of the owner of this file or directory. """) - - if hasattr(os, 'statvfs'): - def statvfs(self): - """ Perform a statvfs() system call on this path. """ - return os.statvfs(self) - - if hasattr(os, 'pathconf'): - def pathconf(self, name): - return os.pathconf(self, name) - - - # --- Modifying operations on files and directories - - def utime(self, times): - """ Set the access and modified times of this file. """ - os.utime(self, times) - - def chmod(self, mode): - os.chmod(self, mode) - - if hasattr(os, 'chown'): - def chown(self, uid, gid): - os.chown(self, uid, gid) - - def rename(self, new): - os.rename(self, new) - - def renames(self, new): - os.renames(self, new) - - - # --- Create/delete operations on directories - - def mkdir(self, mode=0777): - os.mkdir(self, mode) - - def makedirs(self, mode=0777): - os.makedirs(self, mode) - - def rmdir(self): - os.rmdir(self) - - def removedirs(self): - os.removedirs(self) - - - # --- Modifying operations on files - - def touch(self): - """ Set the access/modified times of this file to the current time. - Create the file if it does not exist. - """ - fd = os.open(self, os.O_WRONLY | os.O_CREAT, 0666) - os.close(fd) - os.utime(self, None) - - def remove(self): - os.remove(self) - - def unlink(self): - os.unlink(self) - - - # --- Links - - if hasattr(os, 'link'): - def link(self, newpath): - """ Create a hard link at 'newpath', pointing to this file. """ - os.link(self, newpath) - - if hasattr(os, 'symlink'): - def symlink(self, newlink): - """ Create a symbolic link at 'newlink', pointing here. """ - os.symlink(self, newlink) - - if hasattr(os, 'readlink'): - def readlink(self): - """ Return the path to which this symbolic link points. - - The result may be an absolute or a relative path. - """ - return self.__class__(os.readlink(self)) - - def readlinkabs(self): - """ Return the path to which this symbolic link points. - - The result is always an absolute path. - """ - p = self.readlink() - if p.isabs(): - return p - else: - return (self.parent / p).abspath() - - - # --- High-level functions from shutil - - copyfile = shutil.copyfile - copymode = shutil.copymode - copystat = shutil.copystat - copy = shutil.copy - copy2 = shutil.copy2 - copytree = shutil.copytree - if hasattr(shutil, 'move'): - move = shutil.move - rmtree = shutil.rmtree - - - # --- Special stuff from os - - if hasattr(os, 'chroot'): - def chroot(self): - os.chroot(self) - - if hasattr(os, 'startfile'): - def startfile(self): - os.startfile(self) - +""" path.py - An object representing a path to a file or directory. + +Example: + +from path import path +d = path('/home/guido/bin') +for f in d.files('*.py'): + f.chmod(0o755) + +This module requires Python 2.2 or later. + + +URL: http://www.jorendorff.com/articles/python/path +Author: Jason Orendorff (and others - see the url!) +Date: 9 Mar 2007 +""" + + +# TODO +# - Tree-walking functions don't avoid symlink loops. Matt Harrison +# sent me a patch for this. +# - Bug in write_text(). It doesn't support Universal newline mode. +# - Better error message in listdir() when self isn't a +# directory. (On Windows, the error message really sucks.) +# - Make sure everything has a good docstring. +# - Add methods for regex find and replace. +# - guess_content_type() method? +# - Perhaps support arguments to touch(). + +from __future__ import generators + +# hack P. Lagadec 2009-11-11: removed md5 import to avoid DeprecationWarning with Python 2.6 +# BUT this breaks read_md5()... +#TODO: use hashlib instead when available (Python 2.5+), else fallback to md5 +import sys, warnings, os, fnmatch, glob, shutil, codecs#, md5 + +__version__ = '2.2' +__all__ = ['path'] + +# Platform-specific support for path.owner +if os.name == 'nt': + try: + import win32security + except ImportError: + win32security = None +else: + try: + import pwd + except ImportError: + pwd = None + +# In Python 3, str is already unicode +_base = str +_getcwd = os.getcwd + +# basestring doesn't exist in Python 3; str covers all text strings +try: + basestring +except NameError: + basestring = str + +# Universal newline support (Python 3 handles this by default) +_textmode = 'r' + + +class TreeWalkWarning(Warning): + pass + +class path(_base): + """ Represents a filesystem path. + + For documentation on individual methods, consult their + counterparts in os.path. + """ + + # --- Special Python methods. + + def __repr__(self): + return 'path(%s)' % _base.__repr__(self) + + # Adding a path and a string yields a path. + def __add__(self, more): + try: + resultStr = _base.__add__(self, more) + except TypeError: #Python bug + resultStr = NotImplemented + if resultStr is NotImplemented: + return resultStr + return self.__class__(resultStr) + + def __radd__(self, other): + if isinstance(other, basestring): + return self.__class__(other.__add__(self)) + else: + return NotImplemented + + # The / operator joins paths. + def __div__(self, rel): + """ fp.__div__(rel) == fp / rel == fp.joinpath(rel) + + Join two path components, adding a separator character if + needed. + """ + return self.__class__(os.path.join(self, rel)) + + # Make the / operator work even when true division is enabled. + __truediv__ = __div__ + + def getcwd(cls): + """ Return the current working directory as a path object. """ + return cls(_getcwd()) + getcwd = classmethod(getcwd) + + + # --- Operations on path strings. + + isabs = os.path.isabs + def abspath(self): return self.__class__(os.path.abspath(self)) + def normcase(self): return self.__class__(os.path.normcase(self)) + def normpath(self): return self.__class__(os.path.normpath(self)) + def realpath(self): return self.__class__(os.path.realpath(self)) + def expanduser(self): return self.__class__(os.path.expanduser(self)) + def expandvars(self): return self.__class__(os.path.expandvars(self)) + def dirname(self): return self.__class__(os.path.dirname(self)) + basename = os.path.basename + + def expand(self): + """ Clean up a filename by calling expandvars(), + expanduser(), and normpath() on it. + + This is commonly everything needed to clean up a filename + read from a configuration file, for example. + """ + return self.expandvars().expanduser().normpath() + + def _get_namebase(self): + base, ext = os.path.splitext(self.name) + return base + + def _get_ext(self): + f, ext = os.path.splitext(_base(self)) + return ext + + def _get_drive(self): + drive, r = os.path.splitdrive(self) + return self.__class__(drive) + + parent = property( + dirname, None, None, + """ This path's parent directory, as a new path object. + + For example, path('/usr/local/lib/libpython.so').parent == path('/usr/local/lib') + """) + + name = property( + basename, None, None, + """ The name of this file or directory without the full path. + + For example, path('/usr/local/lib/libpython.so').name == 'libpython.so' + """) + + namebase = property( + _get_namebase, None, None, + """ The same as path.name, but with one file extension stripped off. + + For example, path('/home/guido/python.tar.gz').name == 'python.tar.gz', + but path('/home/guido/python.tar.gz').namebase == 'python.tar' + """) + + ext = property( + _get_ext, None, None, + """ The file extension, for example '.py'. """) + + drive = property( + _get_drive, None, None, + """ The drive specifier, for example 'C:'. + This is always empty on systems that don't use drive specifiers. + """) + + def splitpath(self): + """ p.splitpath() -> Return (p.parent, p.name). """ + parent, child = os.path.split(self) + return self.__class__(parent), child + + def splitdrive(self): + """ p.splitdrive() -> Return (p.drive, ). + + Split the drive specifier from this path. If there is + no drive specifier, p.drive is empty, so the return value + is simply (path(''), p). This is always the case on Unix. + """ + drive, rel = os.path.splitdrive(self) + return self.__class__(drive), rel + + def splitext(self): + """ p.splitext() -> Return (p.stripext(), p.ext). + + Split the filename extension from this path and return + the two parts. Either part may be empty. + + The extension is everything from '.' to the end of the + last path segment. This has the property that if + (a, b) == p.splitext(), then a + b == p. + """ + filename, ext = os.path.splitext(self) + return self.__class__(filename), ext + + def stripext(self): + """ p.stripext() -> Remove one file extension from the path. + + For example, path('/home/guido/python.tar.gz').stripext() + returns path('/home/guido/python.tar'). + """ + return self.splitext()[0] + + if hasattr(os.path, 'splitunc'): + def splitunc(self): + unc, rest = os.path.splitunc(self) + return self.__class__(unc), rest + + def _get_uncshare(self): + unc, r = os.path.splitunc(self) + return self.__class__(unc) + + uncshare = property( + _get_uncshare, None, None, + """ The UNC mount point for this path. + This is empty for paths on local drives. """) + + def joinpath(self, *args): + """ Join two or more path components, adding a separator + character (os.sep) if needed. Returns a new path + object. + """ + return self.__class__(os.path.join(self, *args)) + + def splitall(self): + r""" Return a list of the path components in this path. + + The first item in the list will be a path. Its value will be + either os.curdir, os.pardir, empty, or the root directory of + this path (for example, '/' or 'C:\\'). The other items in + the list will be strings. + + path.path.joinpath(*result) will yield the original path. + """ + parts = [] + loc = self + while loc != os.curdir and loc != os.pardir: + prev = loc + loc, child = prev.splitpath() + if loc == prev: + break + parts.append(child) + parts.append(loc) + parts.reverse() + return parts + + def relpath(self): + """ Return this path as a relative path, + based from the current working directory. + """ + cwd = self.__class__(os.getcwd()) + return cwd.relpathto(self) + + def relpathto(self, dest): + """ Return a relative path from self to dest. + + If there is no relative path from self to dest, for example if + they reside on different drives in Windows, then this returns + dest.abspath(). + """ + origin = self.abspath() + dest = self.__class__(dest).abspath() + + orig_list = origin.normcase().splitall() + # Don't normcase dest! We want to preserve the case. + dest_list = dest.splitall() + + if orig_list[0] != os.path.normcase(dest_list[0]): + # Can't get here from there. + return dest + + # Find the location where the two paths start to differ. + i = 0 + for start_seg, dest_seg in zip(orig_list, dest_list): + if start_seg != os.path.normcase(dest_seg): + break + i += 1 + + # Now i is the point where the two paths diverge. + # Need a certain number of "os.pardir"s to work up + # from the origin to the point of divergence. + segments = [os.pardir] * (len(orig_list) - i) + # Need to add the diverging part of dest_list. + segments += dest_list[i:] + if len(segments) == 0: + # If they happen to be identical, use os.curdir. + relpath = os.curdir + else: + relpath = os.path.join(*segments) + return self.__class__(relpath) + + # --- Listing, searching, walking, and matching + + def listdir(self, pattern=None): + """ D.listdir() -> List of items in this directory. + + Use D.files() or D.dirs() instead if you want a listing + of just files or just subdirectories. + + The elements of the list are path objects. + + With the optional 'pattern' argument, this only lists + items whose names match the given pattern. + """ + names = os.listdir(self) + if pattern is not None: + names = fnmatch.filter(names, pattern) + return [self / child for child in names] + + def dirs(self, pattern=None): + """ D.dirs() -> List of this directory's subdirectories. + + The elements of the list are path objects. + This does not walk recursively into subdirectories + (but see path.walkdirs). + + With the optional 'pattern' argument, this only lists + directories whose names match the given pattern. For + example, d.dirs('build-*'). + """ + return [p for p in self.listdir(pattern) if p.isdir()] + + def files(self, pattern=None): + """ D.files() -> List of the files in this directory. + + The elements of the list are path objects. + This does not walk into subdirectories (see path.walkfiles). + + With the optional 'pattern' argument, this only lists files + whose names match the given pattern. For example, + d.files('*.pyc'). + """ + + return [p for p in self.listdir(pattern) if p.isfile()] + + def walk(self, pattern=None, errors='strict'): + """ D.walk() -> iterator over files and subdirs, recursively. + + The iterator yields path objects naming each child item of + this directory and its descendants. This requires that + D.isdir(). + + This performs a depth-first traversal of the directory tree. + Each directory is returned just before all its children. + + The errors= keyword argument controls behavior when an + error occurs. The default is 'strict', which causes an + exception. The other allowed values are 'warn', which + reports the error via warnings.warn(), and 'ignore'. + """ + if errors not in ('strict', 'warn', 'ignore'): + raise ValueError("invalid errors parameter") + + try: + childList = self.listdir() + except Exception: + if errors == 'ignore': + return + elif errors == 'warn': + warnings.warn( + "Unable to list directory '%s': %s" + % (self, sys.exc_info()[1]), + TreeWalkWarning) + return + else: + raise + + for child in childList: + if pattern is None or child.fnmatch(pattern): + yield child + try: + isdir = child.isdir() + except Exception: + if errors == 'ignore': + isdir = False + elif errors == 'warn': + warnings.warn( + "Unable to access '%s': %s" + % (child, sys.exc_info()[1]), + TreeWalkWarning) + isdir = False + else: + raise + + if isdir: + for item in child.walk(pattern, errors): + yield item + + def walkdirs(self, pattern=None, errors='strict'): + """ D.walkdirs() -> iterator over subdirs, recursively. + + With the optional 'pattern' argument, this yields only + directories whose names match the given pattern. For + example, mydir.walkdirs('*test') yields only directories + with names ending in 'test'. + + The errors= keyword argument controls behavior when an + error occurs. The default is 'strict', which causes an + exception. The other allowed values are 'warn', which + reports the error via warnings.warn(), and 'ignore'. + """ + if errors not in ('strict', 'warn', 'ignore'): + raise ValueError("invalid errors parameter") + + try: + dirs = self.dirs() + except Exception: + if errors == 'ignore': + return + elif errors == 'warn': + warnings.warn( + "Unable to list directory '%s': %s" + % (self, sys.exc_info()[1]), + TreeWalkWarning) + return + else: + raise + + for child in dirs: + if pattern is None or child.fnmatch(pattern): + yield child + for subsubdir in child.walkdirs(pattern, errors): + yield subsubdir + + def walkfiles(self, pattern=None, errors='strict'): + """ D.walkfiles() -> iterator over files in D, recursively. + + The optional argument, pattern, limits the results to files + with names that match the pattern. For example, + mydir.walkfiles('*.tmp') yields only files with the .tmp + extension. + """ + if errors not in ('strict', 'warn', 'ignore'): + raise ValueError("invalid errors parameter") + + try: + childList = self.listdir() + except Exception: + if errors == 'ignore': + return + elif errors == 'warn': + warnings.warn( + "Unable to list directory '%s': %s" + % (self, sys.exc_info()[1]), + TreeWalkWarning) + return + else: + raise + + for child in childList: + try: + isfile = child.isfile() + isdir = not isfile and child.isdir() + except: + if errors == 'ignore': + continue + elif errors == 'warn': + warnings.warn( + "Unable to access '%s': %s" + % (self, sys.exc_info()[1]), + TreeWalkWarning) + continue + else: + raise + + if isfile: + if pattern is None or child.fnmatch(pattern): + yield child + elif isdir: + for f in child.walkfiles(pattern, errors): + yield f + + def fnmatch(self, pattern): + """ Return True if self.name matches the given pattern. + + pattern - A filename pattern with wildcards, + for example '*.py'. + """ + return fnmatch.fnmatch(self.name, pattern) + + def glob(self, pattern): + """ Return a list of path objects that match the pattern. + + pattern - a path relative to this directory, with wildcards. + + For example, path('/users').glob('*/bin/*') returns a list + of all the files users have in their bin directories. + """ + cls = self.__class__ + return [cls(s) for s in glob.glob(_base(self / pattern))] + + + # --- Reading or writing an entire file at once. + + def open(self, mode='r'): + """ Open this file. Return a file object. """ + return open(self, mode) + + def bytes(self): + """ Open this file, read all bytes, return them as a string. """ + f = self.open('rb') + try: + return f.read() + finally: + f.close() + + def write_bytes(self, bytes, append=False): + """ Open this file and write the given bytes to it. + + Default behavior is to overwrite any existing file. + Call p.write_bytes(bytes, append=True) to append instead. + """ + if append: + mode = 'ab' + else: + mode = 'wb' + f = self.open(mode) + try: + f.write(bytes) + finally: + f.close() + + def text(self, encoding=None, errors='strict'): + r""" Open this file, read it in, return the content as a string. + + This uses 'U' mode in Python 2.3 and later, so '\r\n' and '\r' + are automatically translated to '\n'. + + Optional arguments: + + encoding - The Unicode encoding (or character set) of + the file. If present, the content of the file is + decoded and returned as a unicode object; otherwise + it is returned as an 8-bit str. + errors - How to handle Unicode errors; see help(str.decode) + for the options. Default is 'strict'. + """ + if encoding is None: + # 8-bit + f = self.open(_textmode) + try: + return f.read() + finally: + f.close() + else: + # Unicode + f = codecs.open(self, 'r', encoding, errors) + # (Note - Can't use 'U' mode here, since codecs.open + # doesn't support 'U' mode, even in Python 2.3.) + try: + t = f.read() + finally: + f.close() + return (t.replace(u'\r\n', u'\n') + .replace(u'\r\x85', u'\n') + .replace(u'\r', u'\n') + .replace(u'\x85', u'\n') + .replace(u'\u2028', u'\n')) + + def write_text(self, text, encoding=None, errors='strict', linesep=os.linesep, append=False): + r""" Write the given text to this file. + + The default behavior is to overwrite any existing file; + to append instead, use the 'append=True' keyword argument. + + There are two differences between path.write_text() and + path.write_bytes(): newline handling and Unicode handling. + See below. + + Parameters: + + - text - str/unicode - The text to be written. + + - encoding - str - The Unicode encoding that will be used. + This is ignored if 'text' isn't a Unicode string. + + - errors - str - How to handle Unicode encoding errors. + Default is 'strict'. See help(unicode.encode) for the + options. This is ignored if 'text' isn't a Unicode + string. + + - linesep - keyword argument - str/unicode - The sequence of + characters to be used to mark end-of-line. The default is + os.linesep. You can also specify None; this means to + leave all newlines as they are in 'text'. + + - append - keyword argument - bool - Specifies what to do if + the file already exists (True: append to the end of it; + False: overwrite it.) The default is False. + + + --- Newline handling. + + write_text() converts all standard end-of-line sequences + ('\n', '\r', and '\r\n') to your platform's default end-of-line + sequence (see os.linesep; on Windows, for example, the + end-of-line marker is '\r\n'). + + If you don't like your platform's default, you can override it + using the 'linesep=' keyword argument. If you specifically want + write_text() to preserve the newlines as-is, use 'linesep=None'. + + This applies to Unicode text the same as to 8-bit text, except + there are three additional standard Unicode end-of-line sequences: + u'\x85', u'\r\x85', and u'\u2028'. + + (This is slightly different from when you open a file for + writing with fopen(filename, "w") in C or open(filename, 'w') + in Python.) + + + --- Unicode + + If 'text' isn't Unicode, then apart from newline handling, the + bytes are written verbatim to the file. The 'encoding' and + 'errors' arguments are not used and must be omitted. + + If 'text' is Unicode, it is first converted to bytes using the + specified 'encoding' (or the default encoding if 'encoding' + isn't specified). The 'errors' argument applies only to this + conversion. + + """ + if isinstance(text, unicode): + if linesep is not None: + # Convert all standard end-of-line sequences to + # ordinary newline characters. + text = (text.replace(u'\r\n', u'\n') + .replace(u'\r\x85', u'\n') + .replace(u'\r', u'\n') + .replace(u'\x85', u'\n') + .replace(u'\u2028', u'\n')) + text = text.replace(u'\n', linesep) + if encoding is None: + encoding = sys.getdefaultencoding() + bytes = text.encode(encoding, errors) + else: + # It is an error to specify an encoding if 'text' is + # an 8-bit string. + assert encoding is None + + if linesep is not None: + text = (text.replace('\r\n', '\n') + .replace('\r', '\n')) + bytes = text.replace('\n', linesep) + + self.write_bytes(bytes, append) + + def lines(self, encoding=None, errors='strict', retain=True): + r""" Open this file, read all lines, return them in a list. + + Optional arguments: + encoding - The Unicode encoding (or character set) of + the file. The default is None, meaning the content + of the file is read as 8-bit characters and returned + as a list of (non-Unicode) str objects. + errors - How to handle Unicode errors; see help(str.decode) + for the options. Default is 'strict' + retain - If true, retain newline characters; but all newline + character combinations ('\r', '\n', '\r\n') are + translated to '\n'. If false, newline characters are + stripped off. Default is True. + + This uses 'U' mode in Python 2.3 and later. + """ + if encoding is None and retain: + f = self.open(_textmode) + try: + return f.readlines() + finally: + f.close() + else: + return self.text(encoding, errors).splitlines(retain) + + def write_lines(self, lines, encoding=None, errors='strict', + linesep=os.linesep, append=False): + r""" Write the given lines of text to this file. + + By default this overwrites any existing file at this path. + + This puts a platform-specific newline sequence on every line. + See 'linesep' below. + + lines - A list of strings. + + encoding - A Unicode encoding to use. This applies only if + 'lines' contains any Unicode strings. + + errors - How to handle errors in Unicode encoding. This + also applies only to Unicode strings. + + linesep - The desired line-ending. This line-ending is + applied to every line. If a line already has any + standard line ending ('\r', '\n', '\r\n', u'\x85', + u'\r\x85', u'\u2028'), that will be stripped off and + this will be used instead. The default is os.linesep, + which is platform-dependent ('\r\n' on Windows, '\n' on + Unix, etc.) Specify None to write the lines as-is, + like file.writelines(). + + Use the keyword argument append=True to append lines to the + file. The default is to overwrite the file. Warning: + When you use this with Unicode data, if the encoding of the + existing data in the file is different from the encoding + you specify with the encoding= parameter, the result is + mixed-encoding data, which can really confuse someone trying + to read the file later. + """ + if append: + mode = 'ab' + else: + mode = 'wb' + f = self.open(mode) + try: + for line in lines: + isUnicode = isinstance(line, unicode) + if linesep is not None: + # Strip off any existing line-end and add the + # specified linesep string. + if isUnicode: + if line[-2:] in (u'\r\n', u'\x0d\x85'): + line = line[:-2] + elif line[-1:] in (u'\r', u'\n', + u'\x85', u'\u2028'): + line = line[:-1] + else: + if line[-2:] == '\r\n': + line = line[:-2] + elif line[-1:] in ('\r', '\n'): + line = line[:-1] + line += linesep + if isUnicode: + if encoding is None: + encoding = sys.getdefaultencoding() + line = line.encode(encoding, errors) + f.write(line) + finally: + f.close() + + def read_md5(self): + """ Calculate the md5 hash for this file. + + This reads through the entire file. + """ + f = self.open('rb') + try: + m = md5.new() + while True: + d = f.read(8192) + if not d: + break + m.update(d) + finally: + f.close() + return m.digest() + + # --- Methods for querying the filesystem. + + exists = os.path.exists + isdir = os.path.isdir + isfile = os.path.isfile + islink = os.path.islink + ismount = os.path.ismount + + if hasattr(os.path, 'samefile'): + samefile = os.path.samefile + + getatime = os.path.getatime + atime = property( + getatime, None, None, + """ Last access time of the file. """) + + getmtime = os.path.getmtime + mtime = property( + getmtime, None, None, + """ Last-modified time of the file. """) + + if hasattr(os.path, 'getctime'): + getctime = os.path.getctime + ctime = property( + getctime, None, None, + """ Creation time of the file. """) + + getsize = os.path.getsize + size = property( + getsize, None, None, + """ Size of the file, in bytes. """) + + if hasattr(os, 'access'): + def access(self, mode): + """ Return true if current user has access to this path. + + mode - One of the constants os.F_OK, os.R_OK, os.W_OK, os.X_OK + """ + return os.access(self, mode) + + def stat(self): + """ Perform a stat() system call on this path. """ + return os.stat(self) + + def lstat(self): + """ Like path.stat(), but do not follow symbolic links. """ + return os.lstat(self) + + def get_owner(self): + r""" Return the name of the owner of this file or directory. + + This follows symbolic links. + + On Windows, this returns a name of the form ur'DOMAIN\User Name'. + On Windows, a group can own a file or directory. + """ + if os.name == 'nt': + if win32security is None: + raise Exception("path.owner requires win32all to be installed") + desc = win32security.GetFileSecurity( + self, win32security.OWNER_SECURITY_INFORMATION) + sid = desc.GetSecurityDescriptorOwner() + account, domain, typecode = win32security.LookupAccountSid(None, sid) + return domain + u'\\' + account + else: + if pwd is None: + raise NotImplementedError("path.owner is not implemented on this platform.") + st = self.stat() + return pwd.getpwuid(st.st_uid).pw_name + + owner = property( + get_owner, None, None, + """ Name of the owner of this file or directory. """) + + if hasattr(os, 'statvfs'): + def statvfs(self): + """ Perform a statvfs() system call on this path. """ + return os.statvfs(self) + + if hasattr(os, 'pathconf'): + def pathconf(self, name): + return os.pathconf(self, name) + + + # --- Modifying operations on files and directories + + def utime(self, times): + """ Set the access and modified times of this file. """ + os.utime(self, times) + + def chmod(self, mode): + os.chmod(self, mode) + + if hasattr(os, 'chown'): + def chown(self, uid, gid): + os.chown(self, uid, gid) + + def rename(self, new): + os.rename(self, new) + + def renames(self, new): + os.renames(self, new) + + + # --- Create/delete operations on directories + + def mkdir(self, mode=0o777): + os.mkdir(self, mode) + + def makedirs(self, mode=0o777): + os.makedirs(self, mode) + + def rmdir(self): + os.rmdir(self) + + def removedirs(self): + os.removedirs(self) + + + # --- Modifying operations on files + + def touch(self): + """ Set the access/modified times of this file to the current time. + Create the file if it does not exist. + """ + fd = os.open(self, os.O_WRONLY | os.O_CREAT, 0o666) + os.close(fd) + os.utime(self, None) + + def remove(self): + os.remove(self) + + def unlink(self): + os.unlink(self) + + + # --- Links + + if hasattr(os, 'link'): + def link(self, newpath): + """ Create a hard link at 'newpath', pointing to this file. """ + os.link(self, newpath) + + if hasattr(os, 'symlink'): + def symlink(self, newlink): + """ Create a symbolic link at 'newlink', pointing here. """ + os.symlink(self, newlink) + + if hasattr(os, 'readlink'): + def readlink(self): + """ Return the path to which this symbolic link points. + + The result may be an absolute or a relative path. + """ + return self.__class__(os.readlink(self)) + + def readlinkabs(self): + """ Return the path to which this symbolic link points. + + The result is always an absolute path. + """ + p = self.readlink() + if p.isabs(): + return p + else: + return (self.parent / p).abspath() + + + # --- High-level functions from shutil + + copyfile = shutil.copyfile + copymode = shutil.copymode + copystat = shutil.copystat + copy = shutil.copy + copy2 = shutil.copy2 + copytree = shutil.copytree + if hasattr(shutil, 'move'): + move = shutil.move + rmtree = shutil.rmtree + + + # --- Special stuff from os + + if hasattr(os, 'chroot'): + def chroot(self): + os.chroot(self) + + if hasattr(os, 'startfile'): + def startfile(self): + os.startfile(self) + diff --git a/thirdparty/pdfid/pdfid_PL.py b/thirdparty/pdfid/pdfid_PL.py index 79a7f95..6264eda 100644 --- a/thirdparty/pdfid/pdfid_PL.py +++ b/thirdparty/pdfid/pdfid_PL.py @@ -72,7 +72,7 @@ def byte(self): if not inbyte: self.infile.close() return None - return ord(inbyte) + return inbyte if isinstance(inbyte, int) else ord(inbyte) def bytes(self, size): if size <= len(self.ungetted): @@ -80,9 +80,9 @@ def bytes(self, size): del self.ungetted[0:size] return result inbytes = self.infile.read(size - len(self.ungetted)) - if inbytes == '': + if inbytes == b'': self.infile.close() - result = self.ungetted + [ord(b) for b in inbytes] + result = self.ungetted + [b if isinstance(b, int) else ord(b) for b in inbytes] self.ungetted = [] return result @@ -182,7 +182,7 @@ def removeInsideStream(self, byte): self.streamBucket[byte] -= 1 def calc(self): - self.nonStreamBucket = map(operator.sub, self.allBucket, self.streamBucket) + self.nonStreamBucket = list(map(operator.sub, self.allBucket, self.streamBucket)) allCount = sum(self.allBucket) streamCount = sum(self.streamBucket) nonStreamCount = sum(self.nonStreamBucket) @@ -254,7 +254,7 @@ def HexcodeName2String(hexcodeName): return ''.join(map(Hexcode2String, hexcodeName)) def SwapName(wordExact): - return map(SwapCase, wordExact) + return list(map(SwapCase, wordExact)) def UpdateWords(word, wordExact, slash, words, hexcode, allNames, lastName, insideStream, oEntropy, fOut, active_keywords=ACTIVE_KEYWORDS): @@ -280,12 +280,12 @@ def UpdateWords(word, wordExact, slash, words, hexcode, allNames, lastName, if fOut != None: if slash == '/' and '/' + word in active_keywords: wordExactSwapped = HexcodeName2String(SwapName(wordExact)) - fOut.write(wordExactSwapped) + fOut.write(wordExactSwapped.encode() if isinstance(wordExactSwapped, str) else wordExactSwapped) #[PL] avoid print when imported as module if __name__ == '__main__': - print '/%s -> /%s' % (HexcodeName2String(wordExact), wordExactSwapped) + print('/%s -> /%s' % (HexcodeName2String(wordExact), wordExactSwapped)) else: - fOut.write(HexcodeName2String(wordExact)) + fOut.write(HexcodeName2String(wordExact).encode() if isinstance(HexcodeName2String(wordExact), str) else HexcodeName2String(wordExact)) return ('', [], False, lastName, insideStream) class cCVE_2009_3459: @@ -390,7 +390,7 @@ def PDFiD(file, allNames=False, extraData=False, disarm=False, force=False, output_file = pathfile + '.disarmed' + extension fOut = open(output_file, 'wb') for byteHeader in bytesHeader: - fOut.write(chr(byteHeader)) + fOut.write(bytes([byteHeader]) if isinstance(byteHeader, int) else byteHeader.encode()) else: fOut = None if oEntropy != None: @@ -434,12 +434,12 @@ def PDFiD(file, allNames=False, extraData=False, disarm=False, force=False, oBinaryFile.unget(d1) (word, wordExact, hexcode, lastName, insideStream) = UpdateWords(word, wordExact, slash, words, hexcode, allNames, lastName, insideStream, oEntropy, fOut, active_keywords) if disarm: - fOut.write(char) + fOut.write(char.encode("latin-1") if isinstance(char, str) else char) else: oBinaryFile.unget(d1) (word, wordExact, hexcode, lastName, insideStream) = UpdateWords(word, wordExact, slash, words, hexcode, allNames, lastName, insideStream, oEntropy, fOut, active_keywords) if disarm: - fOut.write(char) + fOut.write(char.encode("latin-1") if isinstance(char, str) else char) else: oCVE_2009_3459.Check(lastName, word) @@ -449,7 +449,7 @@ def PDFiD(file, allNames=False, extraData=False, disarm=False, force=False, else: slash = '' if disarm: - fOut.write(char) + fOut.write(char.encode() if isinstance(char, str) else char) if oPDFDate != None and oPDFDate.parse(char) != None: dates.append([oPDFDate.date, lastName]) @@ -536,8 +536,7 @@ def PDFiD(file, allNames=False, extraData=False, disarm=False, force=False, att.nodeValue = str(0) eleKeyword.setAttributeNode(att) if allNames: - keys = words.keys() - keys.sort() + keys = sorted(words.keys()) for word in keys: if not word in keywords: eleKeyword = xmlDoc.createElement('Keyword') @@ -553,7 +552,7 @@ def PDFiD(file, allNames=False, extraData=False, disarm=False, force=False, eleKeyword.setAttributeNode(att) eleDates = xmlDoc.createElement('Dates') xmlDoc.documentElement.appendChild(eleDates) - dates.sort(lambda x, y: cmp(x[0], y[0])) + dates.sort(key=lambda x: x[0]) for date in dates: eleDate = xmlDoc.createElement('Date') eleDates.appendChild(eleDate) @@ -607,9 +606,9 @@ def Scan(directory, allNames, extraData, disarm, force): Scan(os.path.join(directory, entry), allNames, extraData, disarm, force) else: result = PDFiD2String(PDFiD(directory, allNames, extraData, disarm, force), force) - print result + print(result) logfile = open('PDFiD.log', 'a') - print >> logfile, result + logfile.write(result) logfile.close() except: pass @@ -625,21 +624,21 @@ def Main(): if len(args) == 0: if options.disarm: - print 'Option disarm not supported with stdin' + print('Option disarm not supported with stdin') options.disarm = False - print PDFiD2String(PDFiD('', options.all, options.extra, options.disarm, options.force), options.force) + print(PDFiD2String(PDFiD('', options.all, options.extra, options.disarm, options.force), options.force)) elif len(args) == 1: if options.scan: Scan(args[0], options.all, options.extra, options.disarm, options.force) else: - print PDFiD2String(PDFiD(args[0], options.all, options.extra, options.disarm, options.force), options.force) + print(PDFiD2String(PDFiD(args[0], options.all, options.extra, options.disarm, options.force), options.force)) else: oParser.print_help() - print '' - print ' %s' % __description__ - print ' Source code put in the public domain by Didier Stevens, no Copyright' - print ' Use at your own risk' - print ' https://DidierStevens.com' + print('') + print(' %s' % __description__) + print(' Source code put in the public domain by Didier Stevens, no Copyright') + print(' Use at your own risk') + print(' https://DidierStevens.com') return if __name__ == '__main__': diff --git a/thirdparty/plx/plx.py b/thirdparty/plx/plx.py index d780782..8942bf9 100644 --- a/thirdparty/plx/plx.py +++ b/thirdparty/plx/plx.py @@ -1,503 +1,508 @@ -#!/usr/bin/python -# -*- coding: iso-8859-1 -*- -""" -plx - Python portability layer extensions - -v0.14 2008-01-24 Philippe Lagadec - -This module contains several small useful functions to extend Python features, -especially to improve portability on Windows and Unix. - -Project website: http://www.decalage.info/python/plx - -License: CeCILL (open-source GPL compatible), see source code for details. - http://www.cecill.info -""" - -__version__ = '0.14' -__date__ = '2008-01-24' -__author__ = 'Philippe Lagadec' - -#--- LICENSE ------------------------------------------------------------------ - -# Copyright Philippe Lagadec - see http://www.decalage.info/contact for contact info -# -# This software is a Python module/package which contains several small useful -# functions to extend Python features, especially to improve portability on -# Windows and Unix. -# -# This software is governed by the CeCILL license under French law and -# abiding by the rules of distribution of free software. You can use, -# modify and/or redistribute the software under the terms of the CeCILL -# license as circulated by CEA, CNRS and INRIA at the following URL -# "http://www.cecill.info". -# -# A copy of the CeCILL license is also provided in these attached files: -# Licence_CeCILL_V2-en.html and Licence_CeCILL_V2-fr.html -# -# As a counterpart to the access to the source code and rights to copy, -# modify and redistribute granted by the license, users are provided only -# with a limited warranty and the software's author, the holder of the -# economic rights, and the successive licensors have only limited -# liability. -# -# In this respect, the user's attention is drawn to the risks associated -# with loading, using, modifying and/or developing or reproducing the -# software by the user in light of its specific status of free software, -# that may mean that it is complicated to manipulate, and that also -# therefore means that it is reserved for developers and experienced -# professionals having in-depth computer knowledge. Users are therefore -# encouraged to load and test the software's suitability as regards their -# requirements in conditions enabling the security of their systems and/or -# data to be ensured and, more generally, to use and operate it in the -# same conditions as regards security. -# -# The fact that you are presently reading this means that you have had -# knowledge of the CeCILL license and that you accept its terms. - - -#--- CHANGES ------------------------------------------------------------------ - -# 2005-04-08 v0.01 PL: - first version: srt_lat1(), str_oem(), print_oem() -# 2005-07-25 v0.02 PL: - added Popen_timer and stop_pid -# - auto-tests if module launched directly -# 2005-07-28 v0.03 PL: - improved print_oem to avoid exceptions -# 2005-10-21 v0.04 PL: - improved str_lat1() and str_oem() to convert unicode -# or str -# - renamed str_oem and print_oem to str_console and -# print_console (OEM comes from MS-DOS and Windows) -# - codec for str_console is now configured at -# module import, not at each call -# - added unistr() to convert any string to unicode -# 2005-10-23 v0.05 PL: - improved stop_pid() to log errors -# 2006-01-16 v0.06 PL: - added main_is_frozen() and get_main_dir() for py2exe -# 2007-07-24 v0.07 PL: - added get_username to improve portability -# 2007-09-10 v0.08 PL: - fixed OpenProcess args in stop_pid() -# - fixed creationflags portability for Popen_Timer -# 2007-09-18 v0.09 PL: - improved portability for stop_pid -# 2007-10-22 v0.10 PL: - fixed EXIT_KILL_PTIMER to use with F-Prot 6 -# - added display_html_file -# 2007-12-06 v0.11 PL: - added filename check in display_html_file -# 2007-12-16 v0.12 PL: - added main with a few tests -# - fixed a bug in display_html_file on Linux -# - added os.path.abspath in get_main_dir -# 2007-12-17 v0.13 PL: - bugfixes in Popen_timer/stop_pid for Unix -# 2008-01-24 v0.14 PL: - renamed stop_pid to kill_process -# - Popen_timer is now thread-safe (no global var) - -#--- TO DO -------------------------------------------------------------------- - -# + Find a reliable way to set CODEC_CONSOLE according to system settings on -# Windows and Unix ? -# - move tests to a separate script, use unittest -# - test Popen_timer with several threads -# - convert module to package with submodules (e.g. plx.subprocess, plx.test, etc) - -#--- IMPORTS ------------------------------------------------------------------ - -import os, os.path, sys, urllib, imp, webbrowser, threading, signal -from subprocess import * - -# specific modules for Windows: -if sys.platform == 'win32': - try: - import win32api, win32process, win32con - except: - raise ImportError, "the pywin32 module is not installed: "\ - +"see http://sourceforge.net/projects/pywin32" - -# specific modules for Unix: -try: - import pwd -except: - pass - -#--- CONSTANTS ---------------------------------------------------------------- - -# CONSOLE: -# codec used for console display, which depends on OS and on country: -if sys.platform == 'win32': - # on Windows in Western Europe the CMD.exe console uses MS-DOS CP850 encoding: - CODEC_CONSOLE = 'cp850' -elif sys.platform in ('linux2', 'darwin'): - # on Linux and MacOSX it's UTF-8: - CODEC_CONSOLE = 'utf-8' -else: - raise NotImplementedError, \ - "The console display is not configured for this platform (%s)" % sys.platform - -# FOR POPEN_TIMER: -# Default timeout for a process launched Popen_timer (seconds) -POPEN_TIMEOUT = 60 -# Exit code for a process killed by Popen_timer if it reaches the timeout: -if sys.platform == 'win32': - # On Windows this has to be a positive value <255 which is not used by - # used tools. Here we choose a value which is unused by F-Prot 6. - EXIT_KILL_PTIMER = 128+64+32+16+8+4 # old value: 200 -else: - # On Unix this is the value of the SIGKILL signal (-9) used to kill the - # process (negative value): - EXIT_KILL_PTIMER = -signal.SIGKILL -# Default parameter for Popen_Timer: -if sys.platform == 'win32': - # On Windows to launch a process without opening a new window: - CF_CREATE_NO_WINDOW = win32process.CREATE_NO_WINDOW -else: - # On Unix this parameter is not used: - CF_CREATE_NO_WINDOW = 0 - - -#--- GLOBAL VARIABLES --------------------------------------------------------- - - -#=== FUNCTIONS ================================================================ - -def unistr (string, errors='strict', default_codec='latin_1'): - """ - To convert any string (unicode or 8-bit str) in a Unicode string. - If string is str, it will be converted using the specified codec (Latin-1 - by default). A unicode string is returned unchanged. - Any other object is converted using unicode(object). - - @param string: string or object to convert - @type string: str, unicode, or any object - @param errors: see Python doc for unicode() - @type errors: str - @return: converted string - @rtype: unicode - """ - if isinstance(string, unicode): - return string - else: - return unicode(string, default_codec, errors) - - -def str_lat1 (string, errors='strict'): - """ - To convert any string (unicode or 8-bit str) in a str "Latin-1" string. - If string is str, it is returned unchanged. - Any other object is converted using str(object). - - @param string: string or object to convert - @type string: str, unicode, or any object - @param errors: see Python doc for unicode() - @type errors: str - @return: converted string - @rtype: str - """ - if isinstance(string, unicode): - return string.encode('latin_1', errors) - elif isinstance(string, str): - return string - else: - return str(string) - - -def str_console (string, errors='strict', initial_encoding='latin_1'): - """ - To convert any string (unicode or 8-bit str) in a str string with a - suitable encoding for console display ("CP850" on Windows, "UTF-8" on Linux - or MacOSX, ...). - If string is str, it is first decoded using initial_encoding ("Latin-1" by - default). Any other object is converted using unicode(object) first. - - @param string: string or object to convert - @type string: str, unicode, or any object - @param errors: see Python doc for unicode() - @type errors: str - @return: converted string - @rtype: str - """ - ustring = unistr(string, errors, initial_encoding) - return ustring.encode(CODEC_CONSOLE, errors) - - -def print_console (string, errors='strict', initial_encoding='latin_1'): - """ - To print any string (unicode or 8-bit str) on console with a suitable - encoding ("CP850" on Windows, "UTF-8" on Linux or MacOSX, ...). - If string is str, it is first decoded using initial_encoding ("Latin-1" by - default). Any other object is converted using unicode(object) first. - - @param string: string or object to convert - @type string: str, unicode, or any object - @param errors: see Python doc for unicode() - @type errors: str - @return: converted string - @rtype: str - """ - print str_console(string, errors, initial_encoding) - - -def get_username(with_domain=False): - """ - Returns the username of the current logged on user. - Portable on Windows and Unix. - If with_domain=True, on Windows the domain or machine name is added to the - username as "\\domain\user" or "\\machine\user". - """ - # TODO: why not return user@machine on Unix if with_domain=True ? - if sys.platform == 'win32': - # on Windows it is a Win32 call: - if with_domain: - # add domain name if requested: - return '\\\\' + win32api.GetDomainName() + '\\' + win32api.GetUserName() - else: - # else only user name: - return win32api.GetUserName() - else: - # on Unix the info is extracted from /etc/passwd: - uid = os.getuid() - return pwd.getpwuid(uid)[0] - - -def main_is_frozen(): - """ - To determine whether the script is launched from the interpreter or if it - is an executable compiled with py2exe. - See http://www.py2exe.org/index.cgi/HowToDetermineIfRunningFromExe - """ - return (hasattr(sys, "frozen") # new py2exe - or hasattr(sys, "importers") # old py2exe - or imp.is_frozen("__main__")) # tools/freeze - - -def get_main_dir(): - """ - To determine the directory where the main script is located. - Works if it is launched from the interpreter or if it is an executable - compiled with py2exe. - See http://www.py2exe.org/index.cgi/HowToDetermineIfRunningFromExe - """ - if main_is_frozen(): - # script compiled with py2exe: - return os.path.dirname(os.path.abspath(sys.executable)) - else: - # else the script is sys.argv[0] - return os.path.dirname(os.path.abspath(sys.argv[0])) - - -def display_html_file (htmlfile_abspath): - """ - Portable function to display a local HTML file in the default web browser. - Uses os.startfile() on Windows, else webbrowser.open(). - (on Windows, webbrowser only works for http/ftp URLs, not file:///...) - htmlfile_abspath should be an absolute path, at least on Unix. - - WARNING: on Windows, filename MUST have an HTML extension (.html, .htm, - .xml), else the file will be opened in its default application instead of - the web browser. A ValueError exception will be raised otherwise. - """ - #TODO: add os.abspath ? - if sys.platform == 'win32' : - # check extension to avoid launching another application: - if os.path.splitext(htmlfile_abspath.lower())[1] not in ('.html','.htm', - '.xml'): - raise ValueError, 'On Windows, filename extension must be .html,.htm or .xml' - # on Windows, os.startfile is used: - os.startfile(htmlfile_abspath) - else: - # on other OSes, webbrowser.open with a file URL: - file_url = 'file://' + urllib.pathname2url(htmlfile_abspath) - webbrowser.open(file_url) - - -#------------------------------------------------------------------------------ -# KILL_PROCESS -#--------------------- -def kill_process(process, log=None): - """ - To kill a process launched by Popen_timer, if timeout is reached - (POPEN_TIMEOUT). This function is called by a threading.Timer object. - The process terminates and returns EXIT_KILL_PTIMER as errorlevel code. - - process: process object, as created by Popen. - log: optional logging module to log eventual debug and error messages. - (may be the standard logging module, or any compatible object with - exception and debug methods) - """ - # All the process output is logged at debug level, BUT only if stdout - # and stderr were defined as "PIPE" when calling Popen_timer: - if process.stdout and log: - log.debug("Process display:") - log.debug(process.stdout.read()) - if process.stderr and log: - log.debug(process.stderr.read()) - try: - if sys.platform == 'win32': - reqdAccess = win32con.PROCESS_TERMINATE # or PROCESS_ALL_ACCESS ? - #TODO: see MSDN and win32con.py, change reqdAccess if error. - handle = win32api.OpenProcess(reqdAccess, True, process.pid) - win32api.TerminateProcess(handle, EXIT_KILL_PTIMER) - else: - #TODO: a tester pour les autres OS - os.kill(processus.pid, signal.SIGKILL) - if log: - log.debug("Process PID=%d killed." % process.pid) - except: - if log: - # log or display the whole exception: - log.exception("Unable to kill process PID=%d." % process.pid) - # raise exception - raise - - -#------------------------------------------------------------------------------ -# POPEN_TIMER -#--------------------- -def Popen_timer (args, stdin=PIPE , stdout=PIPE, stderr=PIPE, - creationflags = CF_CREATE_NO_WINDOW, - timeout = POPEN_TIMEOUT, log=None): - """ - To launch a process with Popen, with a timeout (see POPEN_TIMEOUT). - If timeout is reached, the process is killed and returns EXIT_KILL_PTIMER. - See subprocess module in standard Python library help for Popen options. - - @param args: process to launch and arguments (list or string) - @param timeout: maximum execution time for process - @param creationflags: parameters for CreateProcess on Windows - @param log: optional logging module to log eventual debug and error messages. - (may be the standard logging module, or any compatible object with - exception and debug methods) - """ - # process is launched with Popen to hide its display: - process = Popen(args, stdin=stdin , stdout=stdout, stderr=stderr, - creationflags=creationflags) - # TODO: handle OSError exception ? - if log: - log.debug("Process launched, PID = %d" % process.pid) - # Timer to kill process if timeout reached: - timer = threading.Timer(timeout, kill_process, args=[process, log]) - timer.start() - if log: - log.debug("Timer started: %d seconds..." % timeout) - result_process = process.wait() - # if process has finished before timeout, timer is cancelled: - timer.cancel() - if log: - log.debug("Exit code returned by process: %d" % result_process) - return result_process - - -def _test_Popen_timer (): - """ - tests for Popen_timer - """ - print 'Tests for Popen_timer:' - print '1) a quick command which ends normally before timeout' - if sys.platform == 'win32': - # Windows - cmd1 = ['cmd.exe', '/c', 'dir'] - else: - # Unix - cmd1 = ['/bin/sh', '-c', 'ls /etc'] - print 'cmd1 = ' + repr(cmd1) - print 'Popen_timer (cmd1)...' - res = Popen_timer (cmd1) - if res == 0: - print 'OK, exit code = 0' - else: - print 'NOK, exit code = %d instead of 0' % res - print '' - - timeout = 3 - print '2) a long command which reaches timeout (%d s)' % timeout - if sys.platform == 'win32': - cmd2 = ['cmd.exe', '/c', 'pause'] - else: - #cmd2 = ['/bin/sh', '-c', 'read -p waiting...'] - cmd2 = ['/bin/sh', '-c', 'read'] - print 'cmd2 = ' + repr(cmd2) - print 'Popen_timer (cmd2, timeout=%d)...' % timeout - res = Popen_timer (cmd2, stdin=None, stdout=None, stderr=None, - timeout=timeout) - if res == EXIT_KILL_PTIMER: - print 'OK, exit code = EXIT_KILL_PTIMER (%d)' % res - else: - print 'NOK, exit code = %d instead of EXIT_KILL_PTIMER (%d)' % (res, - EXIT_KILL_PTIMER) - print '' - - # same tests with logging enabled: - import logging - logging.basicConfig(level=logging.DEBUG) - print '3) a quick command which ends normally before timeout + LOG' - print 'cmd1 = ' + repr(cmd1) - print 'Popen_timer (cmd1)...' - res = Popen_timer (cmd1, log=logging) - if res == 0: - print 'OK, exit code = 0' - else: - print 'NOK, exit code = %d instead of 0' % res - print '' - - timeout = 3 - print '4) a long command which reaches timeout (%d s)' % timeout - print 'cmd2 = ' + repr(cmd2) - print 'Popen_timer (cmd2, timeout=%d)...' % timeout - res = Popen_timer (cmd2, stdin=None, stdout=None, stderr=None, - timeout=timeout, log=logging) - if res == EXIT_KILL_PTIMER: - print 'OK, exit code = EXIT_KILL_PTIMER (%d)' % res - else: - print 'NOK, exit code = %d instead of EXIT_KILL_PTIMER (%d)' % (res, - EXIT_KILL_PTIMER) - print '' - - -#=== MAIN ===================================================================== - -if __name__ == "__main__": - print __doc__ - # A few tests: - print '-'*79 - print 'Tests for module "%s" :' % __file__ - print '-'*79 - print '' - - print "get_username() =", get_username() - print "get_username(with_domain=True) =", get_username(with_domain=True) - print '' - - print "main_is_frozen() =", main_is_frozen() - print "get_main_dir() =", get_main_dir() - print '' - - print 'Test str and console functions:' - str_accents = 'éèêëçà' - ustr_accents = u'éèêëçà' - assert isinstance(unistr(str_accents), unicode) - print_console(str_accents) - print_console(ustr_accents) - print_console(str_lat1(ustr_accents)) - print_console(unistr(str_accents)) - print_console(unistr(ustr_accents)) - print '' - - # Test Popen_timer: - _test_Popen_timer() - print '' - - print "Tests for display_html_file():" - if sys.platform == 'win32' : - # on Windows, check that extensions except html, htm, xml are not - # allowed: - try: - display_html_file('c:\\boot.ini') - print 'NOK: any file extension is allowed !' - except ValueError: - print 'OK: extensions are checked by display_html_file.' - filename = 'test_plx.html' - print "should now open %s in the default browser." % filename - try: - raw_input('Press enter to launch browser... (or Ctrl+C to stop)') - f = open(filename, 'w') - f.write('Test plx.display_html_file') - f.close() - display_html_file(os.path.abspath(filename)) - os.remove(filename) - except KeyboardInterrupt: - print '\nstopped.' - - +#!/usr/bin/python +# -*- coding: iso-8859-1 -*- +""" +plx - Python portability layer extensions + +v0.14 2008-01-24 Philippe Lagadec + +This module contains several small useful functions to extend Python features, +especially to improve portability on Windows and Unix. + +Project website: http://www.decalage.info/python/plx + +License: CeCILL (open-source GPL compatible), see source code for details. + http://www.cecill.info +""" + +__version__ = '0.14' +__date__ = '2008-01-24' +__author__ = 'Philippe Lagadec' + +#--- LICENSE ------------------------------------------------------------------ + +# Copyright Philippe Lagadec - see http://www.decalage.info/contact for contact info +# +# This software is a Python module/package which contains several small useful +# functions to extend Python features, especially to improve portability on +# Windows and Unix. +# +# This software is governed by the CeCILL license under French law and +# abiding by the rules of distribution of free software. You can use, +# modify and/or redistribute the software under the terms of the CeCILL +# license as circulated by CEA, CNRS and INRIA at the following URL +# "http://www.cecill.info". +# +# A copy of the CeCILL license is also provided in these attached files: +# Licence_CeCILL_V2-en.html and Licence_CeCILL_V2-fr.html +# +# As a counterpart to the access to the source code and rights to copy, +# modify and redistribute granted by the license, users are provided only +# with a limited warranty and the software's author, the holder of the +# economic rights, and the successive licensors have only limited +# liability. +# +# In this respect, the user's attention is drawn to the risks associated +# with loading, using, modifying and/or developing or reproducing the +# software by the user in light of its specific status of free software, +# that may mean that it is complicated to manipulate, and that also +# therefore means that it is reserved for developers and experienced +# professionals having in-depth computer knowledge. Users are therefore +# encouraged to load and test the software's suitability as regards their +# requirements in conditions enabling the security of their systems and/or +# data to be ensured and, more generally, to use and operate it in the +# same conditions as regards security. +# +# The fact that you are presently reading this means that you have had +# knowledge of the CeCILL license and that you accept its terms. + + +#--- CHANGES ------------------------------------------------------------------ + +# 2005-04-08 v0.01 PL: - first version: srt_lat1(), str_oem(), print_oem() +# 2005-07-25 v0.02 PL: - added Popen_timer and stop_pid +# - auto-tests if module launched directly +# 2005-07-28 v0.03 PL: - improved print_oem to avoid exceptions +# 2005-10-21 v0.04 PL: - improved str_lat1() and str_oem() to convert unicode +# or str +# - renamed str_oem and print_oem to str_console and +# print_console (OEM comes from MS-DOS and Windows) +# - codec for str_console is now configured at +# module import, not at each call +# - added unistr() to convert any string to unicode +# 2005-10-23 v0.05 PL: - improved stop_pid() to log errors +# 2006-01-16 v0.06 PL: - added main_is_frozen() and get_main_dir() for py2exe +# 2007-07-24 v0.07 PL: - added get_username to improve portability +# 2007-09-10 v0.08 PL: - fixed OpenProcess args in stop_pid() +# - fixed creationflags portability for Popen_Timer +# 2007-09-18 v0.09 PL: - improved portability for stop_pid +# 2007-10-22 v0.10 PL: - fixed EXIT_KILL_PTIMER to use with F-Prot 6 +# - added display_html_file +# 2007-12-06 v0.11 PL: - added filename check in display_html_file +# 2007-12-16 v0.12 PL: - added main with a few tests +# - fixed a bug in display_html_file on Linux +# - added os.path.abspath in get_main_dir +# 2007-12-17 v0.13 PL: - bugfixes in Popen_timer/stop_pid for Unix +# 2008-01-24 v0.14 PL: - renamed stop_pid to kill_process +# - Popen_timer is now thread-safe (no global var) + +#--- TO DO -------------------------------------------------------------------- + +# + Find a reliable way to set CODEC_CONSOLE according to system settings on +# Windows and Unix ? +# - move tests to a separate script, use unittest +# - test Popen_timer with several threads +# - convert module to package with submodules (e.g. plx.subprocess, plx.test, etc) + +#--- IMPORTS ------------------------------------------------------------------ + +import os, os.path, sys, urllib, urllib.request, webbrowser, threading, signal +try: + import imp +except ImportError: + import importlib as imp # Python 3.12+ compatibility +from subprocess import * + +# specific modules for Windows: +if sys.platform == 'win32': + try: + import win32api, win32process, win32con + except: + raise ImportError("the pywin32 module is not installed: " + +"see http://sourceforge.net/projects/pywin32") + +# specific modules for Unix: +try: + import pwd +except: + pass + +#--- CONSTANTS ---------------------------------------------------------------- + +# CONSOLE: +# codec used for console display, which depends on OS and on country: +if sys.platform == 'win32': + # on Windows in Western Europe the CMD.exe console uses MS-DOS CP850 encoding: + CODEC_CONSOLE = 'cp850' +elif sys.platform in ('linux2', 'darwin'): + # on Linux and MacOSX it's UTF-8: + CODEC_CONSOLE = 'utf-8' +else: + raise NotImplementedError( + "The console display is not configured for this platform (%s)" % sys.platform) + +# FOR POPEN_TIMER: +# Default timeout for a process launched Popen_timer (seconds) +POPEN_TIMEOUT = 60 +# Exit code for a process killed by Popen_timer if it reaches the timeout: +if sys.platform == 'win32': + # On Windows this has to be a positive value <255 which is not used by + # used tools. Here we choose a value which is unused by F-Prot 6. + EXIT_KILL_PTIMER = 128+64+32+16+8+4 # old value: 200 +else: + # On Unix this is the value of the SIGKILL signal (-9) used to kill the + # process (negative value): + EXIT_KILL_PTIMER = -signal.SIGKILL +# Default parameter for Popen_Timer: +if sys.platform == 'win32': + # On Windows to launch a process without opening a new window: + CF_CREATE_NO_WINDOW = win32process.CREATE_NO_WINDOW +else: + # On Unix this parameter is not used: + CF_CREATE_NO_WINDOW = 0 + + +#--- GLOBAL VARIABLES --------------------------------------------------------- + + +#=== FUNCTIONS ================================================================ + +def unistr (string, errors='strict', default_codec='latin_1'): + """ + To convert any string (unicode or 8-bit str) in a Unicode string. + If string is str, it will be converted using the specified codec (Latin-1 + by default). A unicode string is returned unchanged. + Any other object is converted using unicode(object). + + @param string: string or object to convert + @type string: str, unicode, or any object + @param errors: see Python doc for unicode() + @type errors: str + @return: converted string + @rtype: unicode + """ + if isinstance(string, str): + return string + elif isinstance(string, bytes): + return string.decode(default_codec, errors) + else: + return str(string) + + +def str_lat1 (string, errors='strict'): + """ + To convert any string (unicode or 8-bit str) in a str "Latin-1" string. + If string is str, it is returned unchanged. + Any other object is converted using str(object). + + @param string: string or object to convert + @type string: str, unicode, or any object + @param errors: see Python doc for unicode() + @type errors: str + @return: converted string + @rtype: str + """ + if isinstance(string, str): + return string + elif isinstance(string, bytes): + return string.decode('latin_1', errors) + else: + return str(string) + + +def str_console (string, errors='strict', initial_encoding='latin_1'): + """ + To convert any string (unicode or 8-bit str) in a str string with a + suitable encoding for console display ("CP850" on Windows, "UTF-8" on Linux + or MacOSX, ...). + If string is str, it is first decoded using initial_encoding ("Latin-1" by + default). Any other object is converted using unicode(object) first. + + @param string: string or object to convert + @type string: str, unicode, or any object + @param errors: see Python doc for unicode() + @type errors: str + @return: converted string + @rtype: str + """ + return unistr(string, errors, initial_encoding) + + +def print_console (string, errors='strict', initial_encoding='latin_1'): + """ + To print any string (unicode or 8-bit str) on console with a suitable + encoding ("CP850" on Windows, "UTF-8" on Linux or MacOSX, ...). + If string is str, it is first decoded using initial_encoding ("Latin-1" by + default). Any other object is converted using unicode(object) first. + + @param string: string or object to convert + @type string: str, unicode, or any object + @param errors: see Python doc for unicode() + @type errors: str + @return: converted string + @rtype: str + """ + print(str_console(string, errors, initial_encoding)) + + +def get_username(with_domain=False): + """ + Returns the username of the current logged on user. + Portable on Windows and Unix. + If with_domain=True, on Windows the domain or machine name is added to the + username as "\\\\domain\\user" or "\\\\machine\\user". + """ + # TODO: why not return user@machine on Unix if with_domain=True ? + if sys.platform == 'win32': + # on Windows it is a Win32 call: + if with_domain: + # add domain name if requested: + return '\\\\' + win32api.GetDomainName() + '\\' + win32api.GetUserName() + else: + # else only user name: + return win32api.GetUserName() + else: + # on Unix the info is extracted from /etc/passwd: + uid = os.getuid() + return pwd.getpwuid(uid)[0] + + +def main_is_frozen(): + """ + To determine whether the script is launched from the interpreter or if it + is an executable compiled with py2exe. + See http://www.py2exe.org/index.cgi/HowToDetermineIfRunningFromExe + """ + return (hasattr(sys, "frozen") # new py2exe + or hasattr(sys, "importers") # old py2exe + or (hasattr(imp, "is_frozen") and imp.is_frozen("__main__"))) # tools/freeze + + +def get_main_dir(): + """ + To determine the directory where the main script is located. + Works if it is launched from the interpreter or if it is an executable + compiled with py2exe. + See http://www.py2exe.org/index.cgi/HowToDetermineIfRunningFromExe + """ + if main_is_frozen(): + # script compiled with py2exe: + return os.path.dirname(os.path.abspath(sys.executable)) + else: + # else the script is sys.argv[0] + return os.path.dirname(os.path.abspath(sys.argv[0])) + + +def display_html_file (htmlfile_abspath): + """ + Portable function to display a local HTML file in the default web browser. + Uses os.startfile() on Windows, else webbrowser.open(). + (on Windows, webbrowser only works for http/ftp URLs, not file:///...) + htmlfile_abspath should be an absolute path, at least on Unix. + + WARNING: on Windows, filename MUST have an HTML extension (.html, .htm, + .xml), else the file will be opened in its default application instead of + the web browser. A ValueError exception will be raised otherwise. + """ + #TODO: add os.abspath ? + if sys.platform == 'win32' : + # check extension to avoid launching another application: + if os.path.splitext(htmlfile_abspath.lower())[1] not in ('.html','.htm', + '.xml'): + raise ValueError('On Windows, filename extension must be .html,.htm or .xml') + # on Windows, os.startfile is used: + os.startfile(htmlfile_abspath) + else: + # on other OSes, webbrowser.open with a file URL: + file_url = 'file://' + urllib.request.pathname2url(htmlfile_abspath) + webbrowser.open(file_url) + + +#------------------------------------------------------------------------------ +# KILL_PROCESS +#--------------------- +def kill_process(process, log=None): + """ + To kill a process launched by Popen_timer, if timeout is reached + (POPEN_TIMEOUT). This function is called by a threading.Timer object. + The process terminates and returns EXIT_KILL_PTIMER as errorlevel code. + + process: process object, as created by Popen. + log: optional logging module to log eventual debug and error messages. + (may be the standard logging module, or any compatible object with + exception and debug methods) + """ + # All the process output is logged at debug level, BUT only if stdout + # and stderr were defined as "PIPE" when calling Popen_timer: + if process.stdout and log: + log.debug("Process display:") + log.debug(process.stdout.read()) + if process.stderr and log: + log.debug(process.stderr.read()) + try: + if sys.platform == 'win32': + reqdAccess = win32con.PROCESS_TERMINATE # or PROCESS_ALL_ACCESS ? + #TODO: see MSDN and win32con.py, change reqdAccess if error. + handle = win32api.OpenProcess(reqdAccess, True, process.pid) + win32api.TerminateProcess(handle, EXIT_KILL_PTIMER) + else: + #TODO: a tester pour les autres OS + os.kill(processus.pid, signal.SIGKILL) + if log: + log.debug("Process PID=%d killed." % process.pid) + except: + if log: + # log or display the whole exception: + log.exception("Unable to kill process PID=%d." % process.pid) + # raise exception + raise + + +#------------------------------------------------------------------------------ +# POPEN_TIMER +#--------------------- +def Popen_timer (args, stdin=PIPE , stdout=PIPE, stderr=PIPE, + creationflags = CF_CREATE_NO_WINDOW, + timeout = POPEN_TIMEOUT, log=None): + """ + To launch a process with Popen, with a timeout (see POPEN_TIMEOUT). + If timeout is reached, the process is killed and returns EXIT_KILL_PTIMER. + See subprocess module in standard Python library help for Popen options. + + @param args: process to launch and arguments (list or string) + @param timeout: maximum execution time for process + @param creationflags: parameters for CreateProcess on Windows + @param log: optional logging module to log eventual debug and error messages. + (may be the standard logging module, or any compatible object with + exception and debug methods) + """ + # process is launched with Popen to hide its display: + process = Popen(args, stdin=stdin , stdout=stdout, stderr=stderr, + creationflags=creationflags) + # TODO: handle OSError exception ? + if log: + log.debug("Process launched, PID = %d" % process.pid) + # Timer to kill process if timeout reached: + timer = threading.Timer(timeout, kill_process, args=[process, log]) + timer.start() + if log: + log.debug("Timer started: %d seconds..." % timeout) + result_process = process.wait() + # if process has finished before timeout, timer is cancelled: + timer.cancel() + if log: + log.debug("Exit code returned by process: %d" % result_process) + return result_process + + +def _test_Popen_timer (): + """ + tests for Popen_timer + """ + print('Tests for Popen_timer:') + print('1) a quick command which ends normally before timeout') + if sys.platform == 'win32': + # Windows + cmd1 = ['cmd.exe', '/c', 'dir'] + else: + # Unix + cmd1 = ['/bin/sh', '-c', 'ls /etc'] + print('cmd1 = ' + repr(cmd1)) + print('Popen_timer (cmd1)...') + res = Popen_timer (cmd1) + if res == 0: + print('OK, exit code = 0') + else: + print('NOK, exit code = %d instead of 0' % res) + print('') + + timeout = 3 + print('2) a long command which reaches timeout (%d s)' % timeout) + if sys.platform == 'win32': + cmd2 = ['cmd.exe', '/c', 'pause'] + else: + #cmd2 = ['/bin/sh', '-c', 'read -p waiting...'] + cmd2 = ['/bin/sh', '-c', 'read'] + print('cmd2 = ' + repr(cmd2)) + print('Popen_timer (cmd2, timeout=%d)...' % timeout) + res = Popen_timer (cmd2, stdin=None, stdout=None, stderr=None, + timeout=timeout) + if res == EXIT_KILL_PTIMER: + print('OK, exit code = EXIT_KILL_PTIMER (%d)' % res) + else: + print('NOK, exit code = %d instead of EXIT_KILL_PTIMER (%d)' % (res, + EXIT_KILL_PTIMER)) + print('') + + # same tests with logging enabled: + import logging + logging.basicConfig(level=logging.DEBUG) + print('3) a quick command which ends normally before timeout + LOG') + print('cmd1 = ' + repr(cmd1)) + print('Popen_timer (cmd1)...') + res = Popen_timer (cmd1, log=logging) + if res == 0: + print('OK, exit code = 0') + else: + print('NOK, exit code = %d instead of 0' % res) + print('') + + timeout = 3 + print('4) a long command which reaches timeout (%d s)' % timeout) + print('cmd2 = ' + repr(cmd2)) + print('Popen_timer (cmd2, timeout=%d)...' % timeout) + res = Popen_timer (cmd2, stdin=None, stdout=None, stderr=None, + timeout=timeout, log=logging) + if res == EXIT_KILL_PTIMER: + print('OK, exit code = EXIT_KILL_PTIMER (%d)' % res) + else: + print('NOK, exit code = %d instead of EXIT_KILL_PTIMER (%d)' % (res, + EXIT_KILL_PTIMER)) + print('') + + +#=== MAIN ===================================================================== + +if __name__ == "__main__": + print(__doc__) + # A few tests: + print('-'*79) + print('Tests for module "%s" :' % __file__) + print('-'*79) + print('') + + print("get_username() =", get_username()) + print("get_username(with_domain=True) =", get_username(with_domain=True)) + print('') + + print("main_is_frozen() =", main_is_frozen()) + print("get_main_dir() =", get_main_dir()) + print('') + + print('Test str and console functions:') + str_accents = 'éèêëçà' + ustr_accents = u'éèêëçà' + assert isinstance(unistr(str_accents), unicode) + print_console(str_accents) + print_console(ustr_accents) + print_console(str_lat1(ustr_accents)) + print_console(unistr(str_accents)) + print_console(unistr(ustr_accents)) + print('') + + # Test Popen_timer: + _test_Popen_timer() + print('') + + print("Tests for display_html_file():") + if sys.platform == 'win32' : + # on Windows, check that extensions except html, htm, xml are not + # allowed: + try: + display_html_file('c:\\boot.ini') + print('NOK: any file extension is allowed !') + except ValueError: + print('OK: extensions are checked by display_html_file.') + filename = 'test_plx.html' + print("should now open %s in the default browser." % filename) + try: + raw_input('Press enter to launch browser... (or Ctrl+C to stop)') + f = open(filename, 'w') + f.write('Test plx.display_html_file') + f.close() + display_html_file(os.path.abspath(filename)) + os.remove(filename) + except KeyboardInterrupt: + print('\nstopped.') + + # This module was coded while listening at Spoon "Ga ga ga ga ga" album. ;-) \ No newline at end of file diff --git a/thirdparty/pyclamd/pyclamd.py b/thirdparty/pyclamd/pyclamd.py index 8a263af..9bf8881 100644 --- a/thirdparty/pyclamd/pyclamd.py +++ b/thirdparty/pyclamd/pyclamd.py @@ -110,16 +110,14 @@ clamd_PORT=3310 clamd_timeout = None #[PL] default timeout for sockets: None = blocking operations -# Eicar test string (encoded for skipping virus scanners) -EICAR = 'WDVPIVAlQEFQWzRcUFpYNTQoUF4pN0NDKTd9JEVJQ0FSLVNUQU5E'.decode('base64') \ - +'QVJELUFOVElWSVJVUy1URVNU\nLUZJTEUhJEgrSCo=\n'.decode('base64') - - ############################################################################ import socket -import types -import string +import base64 + +# Eicar test string (encoded for skipping virus scanners) +EICAR = base64.b64decode('WDVPIVAlQEFQWzRcUFpYNTQoUF4pN0NDKTd9JEVJQ0FSLVNUQU5E') \ + + base64.b64decode('QVJELUFOVElWSVJVUy1URVNU\nLUZJTEUhJEgrSCo=\n') ############################################################################ @@ -140,8 +138,8 @@ def init_unix_socket(filename="/var/run/clamd"): global clamd_PORT global clamd_SOCKET - if type(filename)!=types.StringType: - raise TypeError, 'filename should be a string not "%s"' % filename + if not isinstance(filename, str): + raise TypeError('filename should be a string not "%s"' % filename) use_socket = "UNIX" clamd_SOCKET = filename @@ -171,11 +169,11 @@ def init_network_socket(host='127.0.0.1', port=3310, timeout=None): global clamd_SOCKET global clamd_timeout - if type(host)!=types.StringType: - raise TypeError, 'host should be a string not "%s"' % host + if not isinstance(host, str): + raise TypeError('host should be a string not "%s"' % host) - if type(port)!=types.IntType: - raise TypeError, 'port should be an integer not "%s"' % port + if not isinstance(port, int): + raise TypeError('port should be an integer not "%s"' % port) use_socket = "NET" clamd_HOST = host @@ -207,17 +205,17 @@ def ping(): s = __init_socket__() try: - s.send('PING') - result = s.recv(20000) + s.send(b'PING') + result = s.recv(20000).decode('utf-8', errors='replace') s.close() except: - raise ScanError, 'Could not ping clamd server' + raise ScanError('Could not ping clamd server') if result=='PONG\n': return True else: - raise ScanError, 'Could not ping clamd server' + raise ScanError('Could not ping clamd server') ############################################################################ @@ -238,8 +236,8 @@ def version(): s = __init_socket__() - s.send('VERSION') - result = s.recv(20000).strip() + s.send(b'VERSION') + result = s.recv(20000).decode('utf-8', errors='replace').strip() s.close() return result @@ -261,8 +259,8 @@ def reload(): s = __init_socket__() - s.send('RELOAD') - result = s.recv(20000).strip() + s.send(b'RELOAD') + result = s.recv(20000).decode('utf-8', errors='replace').strip() s.close() return result @@ -284,8 +282,8 @@ def shutdown(): s = __init_socket__() - s.send('SHUTDOWN') - result = s.recv(20000) + s.send(b'SHUTDOWN') + s.recv(20000) s.close() return @@ -315,16 +313,16 @@ def scan_file(file): s = __init_socket__() - s.send('SCAN %s' % file) + s.send(('SCAN %s' % file).encode()) result='...' dr={} while result!='': - result = s.recv(20000) + result = s.recv(20000).decode('utf-8', errors='replace') if len(result)>0: - filenm = string.join(result.strip().split(':')[:-1]) + filenm = ':'.join(result.strip().split(':')[:-1]) virusname = result.strip().split(':')[-1].strip() if virusname[-5:]=='ERROR': - raise ScanError, virusname + raise ScanError(virusname) elif virusname[-5:]=='FOUND': dr[filenm]=virusname[:-6] s.close() @@ -357,16 +355,16 @@ def contscan_file(file): s = __init_socket__() - s.send('CONTSCAN %s' % file) + s.send(('CONTSCAN %s' % file).encode()) result='...' dr={} while result!='': - result = s.recv(20000) + result = s.recv(20000).decode('utf-8', errors='replace') if len(result)>0: - filenm = string.join(result.strip().split(':')[:-1]) + filenm = ':'.join(result.strip().split(':')[:-1]) virusname = result.strip().split(':')[-1].strip() if virusname[-5:]=='ERROR': - raise ScanError, virusname + raise ScanError(virusname) elif virusname[-5:]=='FOUND': dr[filenm]=virusname[:-6] s.close() @@ -403,8 +401,8 @@ def scan_stream(buffer): s = __init_socket__() - s.send('STREAM') - port = int(s.recv(200).strip().split(' ')[1]) + s.send(b'STREAM') + port = int(s.recv(200).decode('utf-8', errors='replace').strip().split(' ')[1]) n=socket.socket(socket.AF_INET, socket.SOCK_STREAM) n.connect((clamd_HOST, port)) @@ -417,12 +415,12 @@ def scan_stream(buffer): result='...' dr={} while result!='': - result = s.recv(20000) + result = s.recv(20000).decode('utf-8', errors='replace') if len(result)>0: filenm = result.strip().split(':')[0] virusname = result.strip().split(':')[1].strip() if virusname[-5:]=='ERROR': - raise ScanError, virusname + raise ScanError(virusname) elif virusname!='OK': dr[filenm]=virusname s.close() @@ -453,7 +451,7 @@ def __init_socket__(): try: s.connect(clamd_SOCKET) except socket.error: - raise ScanError, 'Could not reach clamd using unix socket (%s)' % (clamd_SOCKET) + raise ScanError('Could not reach clamd using unix socket (%s)' % (clamd_SOCKET)) elif use_socket=="NET": s=socket.socket(socket.AF_INET, socket.SOCK_STREAM) #[PL] if a global timeout is defined, it is set for the socket @@ -462,9 +460,9 @@ def __init_socket__(): try: s.connect((clamd_HOST, clamd_PORT)) except socket.error: - raise ScanError, 'Could not reach clamd using network (%s, %s)' % (clamd_HOST, clamd_PORT) + raise ScanError('Could not reach clamd using network (%s, %s)' % (clamd_HOST, clamd_PORT)) else: - raise ScanError, 'Could not reach clamd : connexion not initialized' + raise ScanError('Could not reach clamd : connexion not initialized') return s diff --git a/thirdparty/tempfilemgr/tempfilemgr.py b/thirdparty/tempfilemgr/tempfilemgr.py index 333924c..3cfb779 100644 --- a/thirdparty/tempfilemgr/tempfilemgr.py +++ b/thirdparty/tempfilemgr/tempfilemgr.py @@ -147,7 +147,10 @@ def newTempFile (suffix="", prefix=tempfile.template, dir=None, text=False): handle, filename = tempfile.mkstemp(suffix=suffix, prefix=prefix, dir=dir, text=text) _tempfiles.append(filename) # the OS-level file handle returned by mkstemp is converted to a file object - f = os.fdopen(handle, 'w') + # Python 3: open in binary mode by default so callers writing bytes don't fail; + # the 'text' parameter is kept for the rare callers that need text mode. + mode = 'w' if text else 'wb' + f = os.fdopen(handle, mode) return f, filename diff --git a/thirdparty/zipfile_PL/zipfile_PL.py b/thirdparty/zipfile_PL/zipfile_PL.py index 3ef1e21..6b70e1e 100644 --- a/thirdparty/zipfile_PL/zipfile_PL.py +++ b/thirdparty/zipfile_PL/zipfile_PL.py @@ -1,104 +1,104 @@ -#!/usr/bin/python -# -*- coding: iso-8859-1 -*- -""" -zipfile_PL - -Module qui contient la classe L{ZipFile_PL}, -pour améliorer le traitement des archives Zip du module standard zipfile. - -URL du projet: U{http://www.decalage.info/python/zipfile} - -@author: U{Philippe Lagadec} - -@contact: U{Philippe Lagadec} - -@license: CeCILL (open-source compatible GPL) - cf. code source ou fichier LICENCE.txt joint - -@version: 1.01 - -@status: beta -""" -#============================================================================== -__docformat__ = 'epytext en' - -#__author__ = "Philippe Lagadec" -__date__ = "2007-11-02" -__version__ = "1.01" - -#------------------------------------------------------------------------------ -# LICENCE : - -# Copyright Philippe Lagadec 2005-2008 - voir http://www.decalage.info/contact -# -# zipfile_PL est un module Python pour etendre les fonctionnalites du module -# zipfile de la bibliotheque standard Python. -# -# Ce logiciel est régi par la licence CeCILL soumise au droit français et -# respectant les principes de diffusion des logiciels libres. Vous pouvez -# utiliser, modifier et/ou redistribuer ce programme sous les conditions -# de la licence CeCILL telle que diffusée par le CEA, le CNRS et l'INRIA -# sur le site "http://www.cecill.info". Une copie de cette licence est jointe -# dans les fichiers Licence_CeCILL_V2-fr.html et Licence_CeCILL_V2-en.html. -# -# En contrepartie de l'accessibilité au code source et des droits de copie, -# de modification et de redistribution accordés par cette licence, il n'est -# offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, -# seule une responsabilité restreinte pèse sur l'auteur du programme, le -# titulaire des droits patrimoniaux et les concédants successifs. -# -# A cet égard l'attention de l'utilisateur est attirée sur les risques -# associés au chargement, à l'utilisation, à la modification et/ou au -# développement et à la reproduction du logiciel par l'utilisateur étant -# donné sa spécificité de logiciel libre, qui peut le rendre complexe à -# manipuler et qui le réserve donc à des développeurs et des professionnels -# avertis possédant des connaissances informatiques approfondies. Les -# utilisateurs sont donc invités à charger et tester l'adéquation du -# logiciel à leurs besoins dans des conditions permettant d'assurer la -# sécurité de leurs systèmes et ou de leurs données et, plus généralement, -# à l'utiliser et l'exploiter dans les mêmes conditions de sécurité. -# -# Le fait que vous puissiez accéder à cet en-tête signifie que vous avez -# pris connaissance de la licence CeCILL, et que vous en avez accepté les -# termes. - -#------------------------------------------------------------------------------ -# HISTORIQUE: -# 18/11/2005 v0.01 PL: - 1ère version -# 12/01/2007 v1.00 PL: - version 1.00 officielle -# 2007-11-08 v1.01 PL: - ajout licence CeCILL - -#------------------------------------------------------------------------------ -# A FAIRE: -#------------------------------------------------------------------------------ - -#=== IMPORTS ================================================================== - -# modules standards Python: -import zipfile - -#=== CLASSES ================================================================== - -class ZipFile_PL(zipfile.ZipFile): - """ - Classe qui améliore le traitement des archives Zip par rapport à la - classe ZipFile standard de Python: - - Détection des fichiers avec une taille différente du central directory - une fois décompressés. - """ - - def read(self, name): - """Pour décompresser le contenu d'un fichier de l'archive Zip dans une - chaîne. - Si la taille du fichier décompressé est différente de celle déclarée - dans le central directory, une exception zipfile.BadZipFile est levée. - """ - # on appelle d'abord la méthode read() d'origine - fichier = zipfile.ZipFile.read(self, name) - zinfo = self.getinfo(name) - # on vérifie si la taille du fichier décompressé est la même que celle du central directory - if len(fichier) != zinfo.file_size: - raise zipfile.BadZipfile, \ - 'File size in directory (%d) and header (%d) differ.'\ - % (zinfo.file_size, len(fichier)) - return fichier - +#!/usr/bin/python +# -*- coding: iso-8859-1 -*- +""" +zipfile_PL + +Module qui contient la classe L{ZipFile_PL}, +pour améliorer le traitement des archives Zip du module standard zipfile. + +URL du projet: U{http://www.decalage.info/python/zipfile} + +@author: U{Philippe Lagadec} + +@contact: U{Philippe Lagadec} + +@license: CeCILL (open-source compatible GPL) - cf. code source ou fichier LICENCE.txt joint + +@version: 1.01 + +@status: beta +""" +#============================================================================== +__docformat__ = 'epytext en' + +#__author__ = "Philippe Lagadec" +__date__ = "2007-11-02" +__version__ = "1.01" + +#------------------------------------------------------------------------------ +# LICENCE : + +# Copyright Philippe Lagadec 2005-2008 - voir http://www.decalage.info/contact +# +# zipfile_PL est un module Python pour etendre les fonctionnalites du module +# zipfile de la bibliotheque standard Python. +# +# Ce logiciel est régi par la licence CeCILL soumise au droit français et +# respectant les principes de diffusion des logiciels libres. Vous pouvez +# utiliser, modifier et/ou redistribuer ce programme sous les conditions +# de la licence CeCILL telle que diffusée par le CEA, le CNRS et l'INRIA +# sur le site "http://www.cecill.info". Une copie de cette licence est jointe +# dans les fichiers Licence_CeCILL_V2-fr.html et Licence_CeCILL_V2-en.html. +# +# En contrepartie de l'accessibilité au code source et des droits de copie, +# de modification et de redistribution accordés par cette licence, il n'est +# offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, +# seule une responsabilité restreinte pèse sur l'auteur du programme, le +# titulaire des droits patrimoniaux et les concédants successifs. +# +# A cet égard l'attention de l'utilisateur est attirée sur les risques +# associés au chargement, à l'utilisation, à la modification et/ou au +# développement et à la reproduction du logiciel par l'utilisateur étant +# donné sa spécificité de logiciel libre, qui peut le rendre complexe à +# manipuler et qui le réserve donc à des développeurs et des professionnels +# avertis possédant des connaissances informatiques approfondies. Les +# utilisateurs sont donc invités à charger et tester l'adéquation du +# logiciel à leurs besoins dans des conditions permettant d'assurer la +# sécurité de leurs systèmes et ou de leurs données et, plus généralement, +# à l'utiliser et l'exploiter dans les mêmes conditions de sécurité. +# +# Le fait que vous puissiez accéder à cet en-tête signifie que vous avez +# pris connaissance de la licence CeCILL, et que vous en avez accepté les +# termes. + +#------------------------------------------------------------------------------ +# HISTORIQUE: +# 18/11/2005 v0.01 PL: - 1ère version +# 12/01/2007 v1.00 PL: - version 1.00 officielle +# 2007-11-08 v1.01 PL: - ajout licence CeCILL + +#------------------------------------------------------------------------------ +# A FAIRE: +#------------------------------------------------------------------------------ + +#=== IMPORTS ================================================================== + +# modules standards Python: +import zipfile + +#=== CLASSES ================================================================== + +class ZipFile_PL(zipfile.ZipFile): + """ + Classe qui améliore le traitement des archives Zip par rapport à la + classe ZipFile standard de Python: + - Détection des fichiers avec une taille différente du central directory + une fois décompressés. + """ + + def read(self, name): + """Pour décompresser le contenu d'un fichier de l'archive Zip dans une + chaîne. + Si la taille du fichier décompressé est différente de celle déclarée + dans le central directory, une exception zipfile.BadZipFile est levée. + """ + # on appelle d'abord la méthode read() d'origine + fichier = zipfile.ZipFile.read(self, name) + zinfo = self.getinfo(name) + # on vérifie si la taille du fichier décompressé est la même que celle du central directory + if len(fichier) != zinfo.file_size: + raise zipfile.BadZipfile( + 'File size in directory (%d) and header (%d) differ.' + % (zinfo.file_size, len(fichier))) + return fichier + diff --git a/xfweb.py b/xfweb.py index 37b167a..039cd40 100644 --- a/xfweb.py +++ b/xfweb.py @@ -107,13 +107,13 @@ def filter_response(self): elif exitcode == xf.EXITCODE_BLOCKED: # data should be blocked, return a 403 Forbidden response: self.set_response_forbidden(reason='Forbidden by policy') - print 'DATA:' - print self.resp.data + print('DATA:') + print(self.resp.data) elif exitcode == xf.EXITCODE_ERROR: # error during analysis, return a 403 Forbidden response: self.set_response(500, reason='Error during analysis') - print 'DATA:' - print self.resp.data + print('DATA:') + print(self.resp.data) cherryproxy.main(CherryProxy_xf)