Browse Source

transformation python-infra > scrippy_core

pull/1/head
Michael COSTA 5 months ago
parent
commit
5a072f1388
33 changed files with 448 additions and 2535 deletions
  1. +85
    -34
      README.md
  2. +0
    -2
      infra/api/__init__.py
  3. +0
    -57
      infra/api/apiloader.py
  4. +0
    -88
      infra/api/client.py
  5. +0
    -13
      infra/api/validator.py
  6. +0
    -29
      infra/arguments/logaction.py
  7. +0
    -149
      infra/db/__init__.py
  8. +0
    -68
      infra/git/__init__.py
  9. +0
    -6
      infra/mail/__init__.py
  10. +0
    -96
      infra/mail/mailer.py
  11. +0
    -153
      infra/mail/popclient.py
  12. +0
    -128
      infra/mail/spamassassin.py
  13. +0
    -13
      infra/remote/__init__.py
  14. +0
    -211
      infra/remote/ftp.py
  15. +0
    -548
      infra/remote/gncftp.py
  16. +0
    -429
      infra/remote/ssh.py
  17. +0
    -75
      infra/template/__init__.py
  18. +0
    -125
      infra/vmware/__init__.py
  19. +0
    -42
      infra/workspace/__init__.py
  20. +0
    -15
      requirements.txt
  21. +58
    -41
      scrippy_core/__init__.py
  22. +39
    -34
      scrippy_core/arguments/__init__.py
  23. +30
    -35
      scrippy_core/arguments/historyaction.py
  24. +25
    -0
      scrippy_core/arguments/logaction.py
  25. +36
    -37
      scrippy_core/conf/__init__.py
  26. +62
    -46
      scrippy_core/context/__init__.py
  27. +2
    -2
      scrippy_core/error_handler/__init__.py
  28. +28
    -31
      scrippy_core/history/__init__.py
  29. +19
    -18
      scrippy_core/log/__init__.py
  30. +4
    -3
      scrippy_core/log/debuglogger.py
  31. +6
    -5
      scrippy_core/log/infralogger.py
  32. +5
    -2
      scrippy_core/scriptinfo/__init__.py
  33. +49
    -0
      scrippy_core/workspace/__init__.py

+ 85
- 34
README.md View File

@ -1,6 +1,8 @@
# `infra`
![Scrippy, mon ami le scrangourou](./scrippy-core.png "Scrippy, mon ami le scrangourou")
Ensemble de modules écrits en Python3 facilitant l'écriture de scripts de gestion et d'exploitation d'un système d'information.
# `scrippy_core`
`scrippy_core` est le module _Python3 principal du cadriciel _Scrippy_ permettant la normalisation de l'écriture de scripts _Python_. Ce module apporte l'ensemble des fonctionnalités de base telles que la gestion des fichiers de configuration, de logs, d'historiques d'exécution, le contrôle d'accès aux scripts, la gestion des exécution concurrentielles, etc.
## Avertissement
@ -8,11 +10,13 @@ Ce dépôt est un dépôt **public** librement distribué y compris sur Internet
Le code qu'il contient ainsi que l'ensemble des configurations et documentations ne doit pas contenir d'informations sensibles telles que, et sans se limiter à, mots de passe et clefs privées.
## Prérequis
### Système
#### Ubuntu
- python3 (forcément!)
- python3-pip (forcément!)
- libpq-dev
#### Debian et dérivées
- python3
- python3-pip
- python-dev
- build-essential
@ -20,35 +24,27 @@ Le code qu'il contient ainsi que l'ensemble des configurations et documentations
#### Liste des modules nécessaires
- python3
- paramiko
- psycopg2
- jinja2
- setuptools
- PrettyTable
- coloredlogs
- argcomplete
- GitPython
- prettytable
- coloredlogs
- argcomplete
- filelock
## Installation
### Manuelle
1. Récupération du dépôt et installation:
```bash
git clone ssh://git@gitlab-infra.ref.gnc:22022/puppet/python-infra.git
cd python-infra
git clone ssh://git@gitlab-infra.ref.gnc:22022/scrippy/scrippy-core.git
cd scrippy-core
sudo python3 -m pip install -r requirements.txt
sudo python3 ./setup.py build install
# Les répertoires dans /opt/expl doivent être créés par l'utilisateur _root_ (ou via `sudo`)
NC_EXPL_ROOT=/opt/expl
for DIR in bin conf log tmp hist cpt dat mod
do
mkdir -p ${NC_EXPL_ROOT}/${DIR}
done
```
### Avec `pip`
La version de `pip` utilisée doit être la plus à jour possible et `pip` doit être configuré pour utiliser un serveur de dépôt ayant une copie du module `infra`.
La version de `pip` utilisée doit être la plus à jour possible et `pip` doit être configuré pour utiliser un serveur de dépôt ayant une copie du module `scrippy-core`.
- **/etc/pip.conf**
```ini
@ -59,7 +55,61 @@ La version de `pip` utilisée doit être la plus à jour possible et `pip` doit
- **Commande à exécuter pour l'installation**
```bash
sudo pip3 install infra
sudo pip3 install scrippy-core
```
### Configuration de l'environnement
1. Le fichier de configuration de _Scrippy_ `/etc/scrippy/scrippy.yml` doit définir un certain nombres de répertoires qui seront utiles aux scripts reposant sur _Scrippy_.
| Clef | Utilité |
| --------------------- | ------------------------------------------------------------------------- |
| `env::logdir` | Répertoire des journaux d'exécution des scripts basés sur _Scrippy_ |
| `env::histdir` | Répertoire des fichiers d'historisation des exécutions |
| `env::histdir` | Répertoire des fichiers de rapports |
| `env::tmpdir` | Répertoire de base répertoires de travail des scripts basés sur _Scrippy_ |
| `env::templatedirdir` | Répertoire des fichiers modèles |
| `env::confdir` | Répertoire des fichiers de configuration des scripts basés sur _Scrippy_ |
Modèle de fichier de configuration de l'environnement d'exécution _Scrippy_:
```yaml
env:
logdir: /opt/expl/log
histdir: /opt/expl/hist
reportdir: /opt/expl/cpt
tmpdir: /opt/expl/tmp
datadir: /opt/expl/dat
templatedir: /opt/expl/mod
confdir: /opt/expl/conf
```
Les définitions de ces répertoires seront accessible via es variables suivantes:
| Variables |
| -------------------------------- |
| scrippy_core.SCRIPPY_LOGDIR |
| scrippy_core.SCRIPPY_HISTDIR |
| scrippy_core.SCRIPPY_REPORTDIR |
| scrippy_core.SCRIPPY_TMPDIR |
| scrippy_core.SCRIPPY_DATADIR |
| scrippy_core.SCRIPPY_TEMPLATEDIR |
| scrippy_core.SCRIPPY_CONFDIR |
2. Création des répertoires définis dans le fichier de configuration `/etc/scrippy/scrippy.yml` par l'utilisateur _root_ (ou via `sudo`)
Script python de création des répertoires nécessaires :
```python
import os
import yaml
conf_file = "/etc/scrippy/config.yml"
with open(conf_file, "r") as conf:
scrippy_conf = yaml.load(conf, Loader=yaml.FullLoader)
for rep in scrippy_conf["env"]:
os.makedirs(rep, 0o0755)
```
### Activation de l'auto-complétion (facultatif)
@ -78,6 +128,7 @@ Rafraîchissez votre environnement _bash_.
source /etc/profile
```
----
## Formalisme
@ -271,7 +322,7 @@ Dans l'exemple suivant, 2 occurrences du script sont permises. Une troisième ex
Un fichier de configuration est un fichier simple fichier *ini* découpé en autant de sections que nécessaire:
Pour être chargé un tel fichier de configuration doit simplement se trouver dans le répertoire défini par la constante `NC_EXPL_CFG` et avoir le même nom que le script qui doit le charger débarrassé de son extension et suffixé de l'extension `.conf`.
Pour être chargé un tel fichier de configuration doit simplement se trouver dans le répertoire défini par la constante `scrippy_core.SCRIPPY_CONFDIR` et avoir le même nom que le script qui doit le charger débarrassé de son extension et suffixé de l'extension `.conf`.
De cette manière le script `exp_test_logs.py` chargera automatiquement le fichier de configuration `exp_test_logs.conf`.
@ -580,7 +631,7 @@ La journalisation d'exécution s'effectue à partir du [module `logging` de la b
Deux types de journaux sont simultanément disponibles:
- **La sortie standard**: Affichage en couleurs vers `sys.stdout`
- **Le fichier journal**: Un fichier situé dans `infra.NC_EXPL_LOG` dont le nom est extrapolé du nom du script de la manière suivante: `<nom_du_script>_<timestamp>_<pid>.log`.
- **Le fichier journal**: Un fichier situé dans `scrippy_core.SCRIPPY_LOGDIR` dont le nom est extrapolé du nom du script de la manière suivante: `<nom_du_script>_<timestamp>_<pid>.log`.
Plusieurs niveaux de journalisation sont disponibles (Voir la [documentation](https://docs.python.org/3/library/logging.html#logging-levels)) et le niveau de log par défaut est `INFO`.
@ -667,7 +718,7 @@ Pour afficher la traceback il faut que le log level soit à `DEBUG`
## Gestion de l'historisation des exécutions
Le fichier d'historisation des exécutions situé dans `infra.NC_EXPL_HST` sera également créé et nommé `<nom_du_script>.hist`.
Le fichier d'historisation des exécutions situé dans `scrippy_core.SCRIPPY_HISTDIR` sera également créé et nommé `<nom_du_script>.hist`.
Si le fichier d'historisation est préexistant à l'exécution il sera mis à jour avec les paramètres de la nouvelle exécution.
@ -693,10 +744,10 @@ Cet identifiant de session est reporté dans la colonne `session` de l'historiqu
Le nombre d'exécutions conservées dans le fichier d'historisation est de **50** par défaut.
Il est possible de surcharger cette valeur en précisant le nombre de rétention souhaiter à l'aide de la déclaration `with infra.ScriptContext(__file__, histRetention=100) as _context:`
Il est possible de surcharger cette valeur en précisant le nombre de rétention souhaiter à l'aide de la déclaration `with infra.ScriptContext(__file__, retention=100) as _context:`
```python
with infra.ScriptContext(__file__, workspace=True, histRetention=100) as _context:
with infra.ScriptContext(__file__, workspace=True, retention=100) as _context:
main()
```
@ -733,7 +784,7 @@ with infra.ScriptContext(__file__, workspace=True) as _context:
...
```
Dans l'exemple précédent la variable `workspace_path` contiendra le chemin vers le répertoire temporaire de travail dont le nom sera construit de la manière suivante: `NC_EXPL_TMP/<NOM DU SCRIPT>_<SESSION ID>`
Dans l'exemple précédent la variable `workspace_path` contiendra le chemin vers le répertoire temporaire de travail dont le nom sera construit de la manière suivante: `scrippy_core.SCRIPPY_TMPDIR/<NOM DU SCRIPT>_<SESSION ID>`
**Ex**:
```bash
@ -1080,7 +1131,7 @@ En combinant les deux scripts précédents il devient _facile_ de transférer de
Ce module permet la génération de document à partir de fichiers modèles à partir du moteur de rendu *[jinja2](http://jinja.pocoo.org/)*
Pour être utilisables les fichiers modèles devront être situés dans le répertoire `infra.NC_EXPL_MOD`.
Pour être utilisables les fichiers modèles devront être situés dans le répertoire `scrippy_core.SCRIPPY_TEMPLATEDIR`.
Afin de gérer l'interpolation de variables le fichier modèle DOIT accepter un dictionnaire nommé `params` comme paramètre.
@ -1088,7 +1139,7 @@ Ce dictionnaire devra contenir l'ensemble des variables nécessaires au rendu co
Le rendu du fichier modèle est obtenu à partir de l'objet `template.Renderer` dont l'instanciation nécessite le nom d'un fichier modèle
Le fichier modèle sera automatiquement récupéré dans le répertoire "infra.NC_EXPL_MOD" et ne doit donc pas être un chemin absolu mais simplement un le nom du fichier en lui même.
Le fichier modèle sera automatiquement récupéré dans le répertoire `scrippy_core.SCRIPPY_TEMPLATEDIR` et ne doit donc pas être un chemin absolu mais simplement un le nom du fichier en lui même.
Un modèle est un fichier texte simple dont certains passages dûment balisés seront interpolés par les variables passées en paramètres.
@ -1104,7 +1155,7 @@ Les fichiers modèles peuvent inclure:
#### Fichier modèle simple
Avec le fichier modèle suivant nommé *exp_test_script.mod* et placé dans le répertoire `infra.NC_EXPL_MOD`:
Avec le fichier modèle suivant nommé *exp_test_script.mod* et placé dans le répertoire `scrippy_core.SCRIPPY_TEMPLATEDIR`:
```txt
Bonjour {{params.user}}


+ 0
- 2
infra/api/__init__.py View File

@ -1,2 +0,0 @@
from infra.api.client import Client
from infra.api.apiloader import ApiLoader

+ 0
- 57
infra/api/apiloader.py View File

@ -1,57 +0,0 @@
import sys
import yaml
import logging
class ApiLoader:
"""
L'objet ApiLoader permet le chargement d'une API définie à l'aide d'un fichier YAML.
Exemple: https://gitlab-infra.ref.gnc/python-infra/vmware-api.
"""
def __init__(self):
self.api = {}
def _walk_api(self, dic, path=[]):
"""
Parcours la représentation de l'API passée en argument sous forme de dictionnaire.
"""
for k, v in dic.items():
if isinstance(v, list):
for action_list in v:
for action in action_list:
ppath = ".".join(path)
ppath = "{}.{}".format(ppath, action)
path.append(ppath)
self.api[ppath] = action_list[action]
path.pop()
elif isinstance(v, dict):
path.append(k)
self._walk_api(v, path)
path.pop()
def load_api(self, api_definition):
"""
Charge l'API à partir du fichier YAML passé en argument.
"""
logging.info("[+] Chargement de l'API")
logging.info(" '-> {}".format(api_definition))
try:
with open(api_definition, mode="r") as yaml_file:
api_yaml = yaml.load(yaml_file, Loader=yaml.FullLoader)
self._walk_api(api_yaml)
except Exception as e:
logging.critical("Erreur lors du chargement de l'API: [{}] {}".format(e.__class__.__name__, e))
sys.exit(1)
def get_endpoint_info(self, endpoint):
"""
Renvoi les informations du endpoint passé en argument.
Les informations renvoyées sont un dictionnaire tel que:
{"method": <HTTP METHOD>, "url": <URL>}
"""
logging.info("[+] Recuperation des informations pour: {}".format(endpoint))
try:
return self.api[endpoint]
except KeyError as e:
logging.critical("endpoint inconnu: [{}] {}".format(e.__class__.__name__, e))
sys.exit(1)

+ 0
- 88
infra/api/client.py View File

@ -1,88 +0,0 @@
import sys
import logging
import requests
class Client:
"""
Classe permettant l'utilisation d'API REST
https://fr.wikipedia.org/wiki/Representational_state_transfer
"""
def __init__(self, verify=True, exit_on_error=True):
self.exit_on_error = exit_on_error
self.verify = verify
self.session = requests.Session()
def request(self, method, url, params=None, data=None, headers=None, cookies=None, files=None, auth=None, timeout=None, proxies=None, json=None):
"""
Permet d'exécuter une requète HTTP.
:param method
:param: url: L'URL à atteindre
:param: params: Les paramètres (GET) de la requète, defaults to None
:param: data: Les paramètres (POST) de la requète, defaults to None
:param: headers: Les entêtes de la requète, defaults to None
:param: cookies: Les cookies de la requète, defaults to None
:param: file: Le fichier envoyé (POST), defaults to None
:param: auth: Les informations d'authentification (BASIC AUTH) sous forme d'un tuple (user, password), defaults to None
:param: timeout: Le délai maximum à attendre, defaults to None
:param: proxies: La liste des serveurs mandataires utilisés, defaults to None
:param: json: JSON à envoyer dans le corps de la requète (POST), defaults to None
:return: :class:`Response <Response>` object
:rtype: requests.Response
"""
logging.info("[+] Envoi de la requete au serveur")
default_get_kwargs = {"params": params,
"timeout": timeout,
"headers": headers,
"cookies": cookies,
"proxies": proxies,
"auth": auth,
"verify": self.verify}
default_post_kwargs = {"data": data, "json": json, "files": files}
# TO BE IMPLEMENTED:
# "CONNECT": self._connect,
# "OPTIONS": self._options,
# "TRACE": self._trace,
# "HEAD": self._trace,
methods = {"GET": {"method": self._get,
"kwargs": default_get_kwargs},
"POST": {"method": self._post,
"kwargs": {**default_get_kwargs, **default_post_kwargs}},
"PUT": {"method": self._put,
"kwargs": {**default_get_kwargs, **default_post_kwargs}},
"DELETE": {"method": self._delete,
"kwargs": {**default_get_kwargs, **default_post_kwargs}},
"PATCH": {"method": self._patch,
"kwargs": {**default_get_kwargs, **default_post_kwargs}}}
try:
response = None
response = methods[method]["method"](url, **methods[method]["kwargs"])
logging.debug("api.Client: kwargs: {}".format({**default_get_kwargs, **default_post_kwargs}))
logging.debug("api.Client: {}: {}".format(response.url, response.status_code))
response.raise_for_status()
except Exception as e:
if self.exit_on_error:
logging.critical("Erreur lors de la requete: [{}] {}".format(e.__class__.__name__, e))
sys.exit(1)
logging.warning("Erreur lors de la requete: [{}] {}".format(e.__class__.__name__, e))
finally:
return response
def _get(self, url, **kwargs):
return self.session.get(url, **kwargs)
def _post(self, url, **kwargs):
return self.session.post(url, **kwargs)
def _head(self, url, **kwargs):
return self.session.head(url, **kwargs)
def _put(self, url, **kwargs):
return self.session.put(url, **kwargs)
def _delete(self, url, **kwargs):
return self.session.delete(url, **kwargs)
def _patch(self, url, **kwargs):
return self.session.patch(url, **kwargs)

+ 0
- 13
infra/api/validator.py View File

@ -1,13 +0,0 @@
import logging
import jsonschema
def validate(instance, schema):
logging.info("[+] Validation des parametres")
try:
jsonschema.validate(instance=instance, schema=schema)
except Exception:
logging.info(" '-> Parametres invalides")
return False
logging.info(" '-> Parametres valides")
return True

+ 0
- 29
infra/arguments/logaction.py View File

@ -1,29 +0,0 @@
from argparse import Action, SUPPRESS
from infra.history import History
from infra.log import LogConfig
from infra.context import Context
class LogAction(Action):
def __init__(
self,
option_strings,
dest=SUPPRESS,
default=SUPPRESS,
help="Show execution history"):
super(LogAction, self).__init__(
option_strings=option_strings,
dest=dest,
default=default,
nargs='?',
type=str,
metavar=('SESSION (default: last)'),
help=help)
def __call__(self, parser, namespace, session, option_string=None):
_context = Context.getRootContext()
session = session or History().get_last_session()
LogConfig.render_logfile(session)
parser.exit()

+ 0
- 149
infra/db/__init__.py View File

@ -1,149 +0,0 @@
import sys
import logging
import psycopg2
import cx_Oracle
class Db:
def __init__(self, username=None, host=None, database=None, port=None, password=None, service=None, db_type="postgres"):
self.db_types = {
"postgres": {
"connect": self.connect_pgsql,
"execute": self.execute_pgsql,
},
"oracle": {
"connect": self.connect_oracle,
"execute": self.execute_oracle,
},
}
self.username = username
self.host = host
self.database = database
self.port = port
self.password = password
self.service = service
self.db_type = db_type
self.connection = None
self.cursor = None
def __enter__(self):
self.connect()
return self
def __exit__(self, type_err, value, traceback):
del type_err, value, traceback
self.close()
def connect(self):
try:
logging.info("[+] Connexion au service de base de donnees:")
self.connection = self.db_types[self.db_type]["connect"]()
except (psycopg2.Error, cx_Oracle.DatabaseError) as e:
logging.critical(" '-> Erreur lors de la connexion: [{}] {}".format(e.__class__.__name__, e))
sys.exit(1)
def connect_oracle(self):
dsn = cx_Oracle.makedsn(self.host, self.port, service_name=self.database)
logging.info(" '-> {}".format(dsn))
return cx_Oracle.connect(self.username, self.password, dsn)
def connect_pgsql(self):
if self.service:
logging.info(" '-> {}".format(self.service))
return psycopg2.connect(service=self.service)
logging.info(" '-> {}@{}:{}/{}".format(self.username, self.host, self.port, self.database))
return psycopg2.connect(user=self.username,
host=self.host,
port=self.port,
dbname=self.database,
password=self.password)
def close(self):
try:
if self.cursor:
self.cursor.close()
except (psycopg2.Error, cx_Oracle.InterfaceError) as e:
logging.warning(" '-> Erreur lors de la fermeture de la connexion: [{}] {}".format(e.__class__.__name__, e))
finally:
if self.connection:
logging.info("[+] Fermeture de la connexion a la base de donnees")
self.connection.close()
self.connection = None
def execute(self, request, params=None, verbose=False, commit=False):
try:
logging.info("[+] Exécution de la requète:")
result = self.db_types[self.db_type]["execute"](request,
params,
verbose)
if commit:
self.connection.commit()
return result
except Exception as e:
logging.error(" '-> Erreur lors de l'execution de la requete [{}] {}".format(e.__class__.__name__, e))
self.connection.rollback()
self.close()
def execute_oracle(self, request, params, verbose):
with self.connection.cursor() as self.cursor:
if verbose:
logging.info(" '-> {}".format(request))
logging.info(" '-> {}".format(params))
if params is not None:
self.cursor.execute(request, params)
else:
self.cursor.execute(request)
logging.debug(" '-> {} ligne(s) impactee(s)".format(self.cursor.rowcount))
try:
result = self.cursor.fetchall()
except cx_Oracle.InterfaceError as e:
logging.debug(" '-> Aucun resultat a recuperer")
return None
if verbose:
for row in result:
logging.info(" '-> {}".format(" | ".join([str(i) for i in row])))
return result
def execute_pgsql(self, request, params, verbose):
with self.connection.cursor() as self.cursor:
if verbose:
logging.info(" '-> {}".format(self.cursor.mogrify(request, params)))
self.cursor.execute(request, params)
logging.debug(" '-> {} ligne(s) impactee(s)".format(self.cursor.rowcount))
try:
result = self.cursor.fetchall()
except psycopg2.ProgrammingError as e:
logging.debug(" '-> Aucun resultat a recuperer")
return None
if verbose:
for row in result:
logging.info(" '-> {}".format(" | ".join([str(i) for i in row])))
return result
class ConnectionChain:
"""
L'objet ConnectionChain permet de retrouver les informations de connexions à une base de données à partir d'une chaîne de caractères répondant au format <DB_TYPE>:<ROLE>/<PASSWORD>@<HOST>:<PORT>//<DB_NAME>
"""
def __init__(self, connection_chain):
self.db_type = connection_chain.split(':')[0]
self.username = connection_chain.split(':')[1].split('/')[0]
self.password = connection_chain.split('@')[0].split('/')[1]
self.host = connection_chain.split('@')[1].split(':')[0]
self.port = int(connection_chain.split(':')[2].split('/')[0])
self.database = connection_chain.split('/')[-1]
class Db_from_cc(Db):
"""
Classe spécifique permettant l'instanciation d'un objet Db à l'aide d'un objet ConnectionChain.
"""
def __init__(self, connection_chain):
cc = ConnectionChain(connection_chain)
super().__init__(username=cc.username,
host=cc.host,
database=cc.database,
port=cc.port,
password=cc.password,
service=None,
db_type=cc.db_type)

+ 0
- 68
infra/git/__init__.py View File

@ -1,68 +0,0 @@
import os
import git
import sys
import logging
class Repo():
"""Cette classe permet la manipulation d'un dépôt Git, notamment son clonage, l'enregistrement (commit), la récuparation (pull) et l'envoi (push) des modifications apportées.
Cette classe permet uniquement la manipulation d'un dépôt via SSH.
:param username: Le nom d'utilisateur utilisé pour se connecter via SSH sur le serveur Git
:type username: str, obligatoire
:param host: Le nom de l'hôte distant hébergeant le serveur Git
:type host: str, obligatoire
:param port: Le numéro du port sur lequel se connecter, valeur par défaut: 22
:type port: int, optionnel
:param reponame: Le nom du dépôt distant
:type reponame: str, obligatoire
"""
def __init__(self, username=None, host=None, port=22, reponame=None):
self.url = "ssh://{}@{}:{}/{}".format(username, host, port, reponame)
self.name = reponame
self.cloned = None
self.path = None
self.origin = None
self.branch = None
def clone(self, branch, path, origin="origin", options=[]):
logging.info("[+] Clonage du depot: {}".format(self.url))
logging.info(" '-> {}".format(path))
self.path = path
self.branch = branch
try:
self.cloned = git.Repo.clone_from(self.url, path, branch=branch, multi_options=options)
self.origin = self.cloned.remote(name=origin)
except Exception as e:
logging.critical(" '-> Erreur lors du clonage du depot: [{}]: {}".format(e.__class__.__name__, e))
sys.exit(1)
def commit(self, message, error_on_clean_repo=True):
logging.info("[+] Commit: [{}]: {}".format(self.name, message))
if not self.cloned.is_dirty(untracked_files=True):
if error_on_clean_repo:
logging.critical("Impossible de commiter sans modification")
sys.exit(1)
logging.warning(" '-> Aucune modification a commiter")
return
self.cloned.git.add(".")
self.cloned.git.commit(m=message)
def pull(self):
logging.info("[+] Pull: [{}]: {}".format(self.name, self.origin))
self.cloned.git.pull()
def push(self):
logging.info("[+] Push: [{}]: {}".format(self.name, self.origin))
self.origin.push()
def commit_push(self, message, error_on_clean_repo=True):
try:
self.commit(message=message, error_on_clean_repo=error_on_clean_repo)
self.pull()
self.push()
except Exception as e:
logging.critical(" '-> Erreur lors du commit: [{}]: {}".format(e.__class__.__name__, e))
sys.exit(1)

+ 0
- 6
infra/mail/__init__.py View File

@ -1,6 +0,0 @@
#!/usr/bin/env python3
from infra.mail.mailer import Mailer
from infra.mail.popclient import PopClient
from infra.mail.popclient import PopException
from infra.mail.spamassassin import SpamAssassinClient

+ 0
- 96
infra/mail/mailer.py View File

@ -1,96 +0,0 @@
#!/usr/bin/env python3
import ssl
import smtplib
import logging
from datetime import datetime
from email.utils import make_msgid
from email.message import EmailMessage
_DEFAULT_CIPHERS = ('ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+HIGH:'
'DH+HIGH:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+HIGH:RSA+3DES:!aNULL:'
'!eNULL:!MD5'
)
class Mailer:
"""
L'objet Mailer est une interface simplifiée vers la bibliothèque "smtplib" permettant l'envoi de courriels
répondant aux RFC en vigeur.
Par défaut la classe Mailer utilise la machine locale et le port 25 comme serveur SMTP.
"""
def __init__(self, host='localhost', port=25, user=None, password=None, starttls=False, timeout=60):
self.host = host
self.port = port
self.user = user
self.password = password
self.starttls = starttls
self.timeout = timeout
def send(self, subject, body, to_addrs, from_addr):
"""
La méthode "Mailer.send()" se charge d'envoyer le courriel dont les paramètres sont passés en arguments.
"""
if not isinstance(to_addrs, tuple):
to_addrs = (to_addrs,)
tz = "{}00".format(datetime.now().astimezone().tzinfo)
date = datetime.today().strftime("%a, %d %b %Y %H:%M:%S {}").format(tz)
message = EmailMessage()
message['Subject'] = subject
message['From'] = from_addr
message['To'] = to_addrs
message['Date'] = date
message['Message-ID'] = make_msgid()
message.set_content(body)
logging.info("[+] Envoi du message:")
logging.info(" '-> Host: {}:{}".format(self.host, self.port))
logging.debug(" '-> User: {} [{}]".format(self.user, self.password))
logging.info(" '-> To: {}".format(to_addrs))
logging.info(" '-> From: {}".format(from_addr))
logging.info(" '-> Subject: {}".format(subject))
try:
with smtplib.SMTP(self.host, self.port, timeout=self.timeout) as smtp:
if self.starttls:
logging.debug(" '-> Création du context SSL")
# FIXME: Trouver le moyen de vérifier le certificat serveur à partir des CA installées sur le système:
# https://docs.python.org/3/library/ssl.html#best-defaults
context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
context.options |= ssl.OP_NO_SSLv2
context.options |= ssl.OP_NO_SSLv3
context.set_ciphers(_DEFAULT_CIPHERS)
context.set_default_verify_paths()
context.load_default_certs()
# context.verify_mode = ssl.CERT_REQUIRED
context.verify_mode = ssl.CERT_NONE
logging.debug(" '-> STARTTLS...")
response = smtp.starttls(context=context)
logging.debug(" '-> {}".format(response))
logging.debug(" '-> EHLO...")
else:
logging.debug(" '-> Connexion...")
response = smtp.connect(self.host, self.port)
logging.debug(" '-> {}: {}".format(response[0], response[1]))
logging.debug(" '-> HELO...")
smtp.ehlo_or_helo_if_needed()
if self.user:
logging.debug(" '-> Authentification...")
smtp.login(self.user, self.password)
logging.debug(" '-> Envoi du mail...")
result = smtp.send_message(message, from_addr, to_addrs)
if result:
for reject in result:
logging.error(" '-> {}: {}: {}".format(reject, result[reject][0], result[reject][1]))
logging.debug(" '-> Bye...")
smtp.quit()
if result:
return False
return True
except (smtplib.SMTPHeloError, smtplib.SMTPAuthenticationError, smtplib.SMTPNotSupportedError,
smtplib.SMTPRecipientsRefused, smtplib.SMTPHeloError, smtplib.SMTPSenderRefused,
smtplib.SMTPDataError, smtplib.SMTPException, RuntimeError) as e:
logging.error("[{}] {}".format(e.__class__.__name__, e))
return False
except Exception as e:
logging.error("[{}] {}".format(e.__class__.__name__, e))
logging.error("Merci de remonter l'erreur precedente afin qu'elle soit integree au module 'infra'.")
return False

+ 0
- 153
infra/mail/popclient.py View File

@ -1,153 +0,0 @@
#!/usr/bin/env python3
import sys
import time
import socket
import logging
from io import BytesIO
class PopException(Exception):
"""
Classe d'erreur spécifique à la gestion des erreurs POP
"""
def __init__(self, message):
super().__init__(message)
class PopClient:
"""
L'objet PopClient implémente une partie du protocle POP3 (RFC 1939):
https://tools.ietf.org/html/rfc1939
Par défaut le serveur POP3 utilisé est la machine locale '127.0.0.1'.
"""
def __init__(self, host='127.0.0.1', port=110, timeout=2):
self.host = host
self.port = port
self.timeout = timeout
self.socket = None
def connect(self):
"""
Se connecte sur le serveur distant.
"""
logging.info("[+] Connexion au serveur de courriels {}:{}".format(self.host, self.port))
logging.info(" '-> {}:{}".format(self.host, self.port))
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.settimeout(self.timeout)
try:
self.socket.connect((self.host, self.port))
self._recv_data()
except Exception as e:
logging.critical(" '-> Erreur lors de la connexion")
sys.exit(1)
def _send_data(self, data):
"""
Envoie au serveur distant et via le socket les données passées en argument.
Cette méthode est à usage interne et ne devrait pas etre utilisée directement.
"""
try:
self.socket.sendall(data)
logging.debug("Sent: {}".format(data))
except Exception as e:
logging.critical("Erreur lors de la communication avec le serveur de courriels: []: {}".format(e.__class__.__name__, str(e)))
raise e
def _recv_data(self, bufsize=8192):
"""
Reçoit les données envoyées par le serveur distant au travers du socket.
Cette méthode est à usage interne et ne devrait pas etre utilisée directement.
"""
data = b''
start = time.time()
try:
while time.time() - start < self.timeout:
try:
packet = self.socket.recv(bufsize)
data += packet
except Exception as e:
# On recommence jusqu'à expiration du timeout
logging.debug("{}/{}".format(time.time()-start, self.timeout))
logging.debug("[{}]: {}".format(e.__class__.__name__, str(e)))
pass
logging.debug("Received: {}".format(data))
return data
except Exception as e:
logging.critical("Erreur lors de la communication avec le serveur de courriels: []: {}".format(e.__class__.__name__, str(e)))
raise e
def authenticate(self, username, password):
"""
Permet l'authentification de l'utilisateur avec les identifiants 'username' et 'passsword' passés en argument.
Sort en erreur (sys.exit(1)) si l'authentification échoue.
"""
logging.info("[+] Auhentification")
logging.debug(" '-> {}:{}".format(username, password))
buffer = BytesIO()
buffer.write(b'USER %s\r\n' % username.encode())
self._send_data(buffer.getvalue())
resp = self._recv_data()
if resp[:3] != b'+OK':
logging.critical(" '-> Erreur lors de l'authentification: {}".format(resp))
sys.exit(1)
buffer = BytesIO()
buffer.write(b'PASS %s\r\n' % password.encode())
self._send_data(buffer.getvalue())
resp = self._recv_data()
if resp[:3] != b'+OK':
raise PopException("Erreur lors de l'authentification: {}".format(resp))
def stat(self):
"""
Récupère le nombre de messages disponibles dans la boite de l'utilisateur.
Sort en erreur (sys.exit(1)) si l'authentification échoue.
"""
logging.info("[+] Recuperation du nombres de courriels disponibles")
buffer = BytesIO()
buffer.write(b'STAT\r\n')
self._send_data(buffer.getvalue())
resp = self._recv_data()
if resp[:3] != b'+OK':
raise PopException("Erreur lors de la recuperation du nombre de courriels: {}".format(resp))
return resp
def retr(self, num):
"""
Récupère le contenu du courriel dont le numéro est passé en argument.
Sort en erreur (sys.exit(1)) si l'authentification échoue.
"""
logging.info("[+] Recuperation du courriel")
logging.debug(" '-> No: {}".format(num))
buffer = BytesIO()
buffer.write(b'RETR %d \r\n' % num)
self._send_data(buffer.getvalue())
resp = self._recv_data()
if resp[:3] != b'+OK':
raise PopException("Erreur lors de la recuperation du courriel: {}".format(resp))
# On ne renvoie que le mail (sans le code de réponse du serveur)
return resp.split(b'\r\n', 1)[1]
def dele(self, num):
"""
Supprime le contenu du courriel dont le numéro est passé en argument.
Sort en erreur (sys.exit(1)) si l'authentification échoue.
"""
logging.info("[+] Suppression du courriel")
logging.debug(" '-> No: {}".format(num))
buffer = BytesIO()
buffer.write(b'DELE %d\r\n' % num)
self._send_data(buffer.getvalue())
resp = self._recv_data()
if resp[:3] != b'+OK':
raise PopException("Erreur lors de la suppression du courriel: {}".format(resp))
return resp
def quit(self):
"""
Se deconnecte du serveur distant.
"""
logging.info("[+] Deconnexion du serveur de courriels")
buffer = BytesIO()
buffer.write(b'QUIT\r\n')
self._send_data(buffer.getvalue())
self.socket.shutdown(socket.SHUT_WR)

+ 0
- 128
infra/mail/spamassassin.py View File

@ -1,128 +0,0 @@
#!/usr/bin/env python3
import time
import socket
import logging
from io import BytesIO
class SpamAssassinClient:
"""
L'objet SpamAssassinClient implémente une partie du protocole SpamAssassin:
https://svn.apache.org/repos/asf/spamassassin/trunk/spamd/PROTOCOL
Par défaut le serveur SpamAssassin utilisé est la machine locale '127.0.0.1'.
"""
def __init__(self, host='127.0.0.1', port=783, timeout=2):
self.host = host
self.port = port
self.timeout = timeout
self.socket = None
def __enter__(self):
self.connect()
return self
def __exit__(self, type_err, value, traceback):
del type_err, value, traceback
self.close()
def connect(self):
"""
Permet la connexion au serveur SpamAssassin défini au moment de l'instanciation.
"""
logging.info("[+] Connexion au serveur SpamAssassin")
logging.info(" '-> {}:{}".format(self.host, self.port))
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.settimeout(self.timeout)
self.socket.connect((self.host, self.port))
def close(self):
"""
Ferme la connexion au serveur SpamAssassin.
"""
logging.info("[+] Deconnexion du serveur SpamAssassin")
self.socket.shutdown(socket.SHUT_WR)
self.socket.close()
def _send_data(self, data):
"""
Envoie un message (au sens protocole d'échange SpamaAssassin) au serveur SpamaAssassin défini lors de l'instanciation de l'objet.
Cette méthode est utilisée par toutes les méthodes ayant besoin de communiquer avec le serveur SpamAssassin.
https://svn.apache.org/repos/asf/spamassassin/trunk/spamd/PROTOCOL
"""
try:
self.socket.sendall(data)
logging.debug("Sent: {}".format(data))
except Exception as e:
logging.error("Erreur lors de l'envoi du message vers SpamAssassin: [{}]: {}".format(e.__class__.__name__, str(e)))
# on renvoie l'erreur au niveau supérieur
raise e
def _recv_data(self, bufsize=8192):
data = b''
start = time.time()
while time.time() - start < self.timeout:
try:
packet = self.socket.recv(bufsize)
data += packet
except Exception as e:
# On recommence jusqu'à expiration du timeout
logging.debug("{}/{}".format(time.time()-start, self.timeout))
logging.debug("[{}]: {}".format(e.__class__.__name__, str(e)))
pass
logging.debug("Received: {}".format(data))
return data
def learn(self, mail, mail_type):
"""
Donne au serveur SpamAssassin le courriel à apprendre.
https://svn.apache.org/repos/asf/spamassassin/trunk/spamd/PROTOCOL
"""
logging.info("[+] Enregistrement du courriel comme [{}]".format(mail_type.upper()))
try:
buffer = BytesIO()
buffer.write(b'TELL SPAMC/1.3\r\n')
buffer.write(b'Content-Length: %d\r\n' % len(mail))
buffer.write(b'Message-class: %s\r\n' % mail_type.encode())
buffer.write(b'Set: local, remote\r\n\r\n')
buffer.write(mail.encode())
logging.debug(str(buffer.getvalue()))
self._send_data(buffer.getvalue())
resp = self._recv_data()
except Exception as e:
logging.error("Erreur lors de l'apprentissage: [{}]: {}".format(e.__class__.__name__, str(e)))
# on renvoie l'erreur au niveau supérieur
raise e
def check_spam(self, mail):
"""
Vérifie le courriel passé en argument.
Renvoie True sie le courriel est considéré comme un spam et fasle dans le cas contraire.
Le résultat et le score sont inscrits dans le log de debug.
"""
logging.info("[+] Verification du courriel")
results = {"True": True, "False": False}
try:
buffer = BytesIO()
buffer.write(b'TELL SPAMC/1.3\r\n')
buffer.write(b'Content-Length: %d\r\n\r\n' % len(mail))
buffer.write(mail.encode())
logging.debug(str(buffer.getvalue()))
self._send_data(buffer.getvalue())
data = self._recv_data().split("\r\n")
if data[0].split()[3] == b'OK':
result = data[1].split()
score = "{}/{}".format(result[3].decode().strip(),
result[5].decode().strip())
result = results[result[1].decode().strip()]
logging.debug("Resultat: {} | Score: {}".format(result, score))
return result
# Si on arrive ici, c'est qu'une erreur est survenue
# On renvoie l'exception pour qu'elle soit attrapée plus bas.
raise Exception("{}".format(data))
except Exception as e:
logging.error("Erreur lors de la verification: [{}]: {}".format(e.__class__.__name__, str(e)))
# on renvoie l'erreur au niveau supérieur
raise e

+ 0
- 13
infra/remote/__init__.py View File

@ -1,13 +0,0 @@
#!/bin/env python3
"""
Ce module offre l'ensemble des objets, méthodes et fonctions permettant les opérations sur les hotes distants accessibles via SSH/SFTP:
- Exécution de comande sur hôte distant
- Copie de répertoires/fichiers sur hôte distant
- Suppression de répertoires/fichiers sur hôte distant
- Copie de fichiers entre hôtes distants (la machine locale agissant comme tampon)
- ...
"""
from infra.remote.ssh import Ssh
from infra.remote.ftp import Ftp

+ 0
- 211
infra/remote/ftp.py View File

@ -1,211 +0,0 @@
import os
import re
import sys
import logging
from infra.remote.gncftp import GncFtp
class Ftp:
"""
La classe principale permettant la manipulation des hotes distants via FTP
"""
def __init__(self, hostname, port=21, username="anonymous", password="", tls=True, explicit=True):
logging.info("[+] Creation de la connexion:")
self.hostname = hostname
self.port = port
self.username = username
self.password = password
self.tls = tls
self.explicit = explicit
self.remote = GncFtp(self.hostname,
self.port,
self.username,
self.password,
self.tls,
self.explicit)
def __enter__(self):
self.connect()
return self
def __exit__(self, type_err, value, traceback):
del type_err, value, traceback
self.close()
def connect(self, timeout=30):
"""
Permet la connexion FTP a un hôte distant.
"""
connected = False
logging.info("[+] Connexion a {}@{}:{}".format(self.username, self.hostname, self.port))
try:
connected = self.remote.connect()
if connected:
resp = self.remote.login()
logging.debug("[LOGIN] {}".format(resp))
except Exception as e:
logging.critical(" +-> Erreur inattendue: [{}] {}".format(e.__class__.__name__, e))
logging.critical(" '-> Merci de reporter l'erreur ci-dessus afin qu'elle soit integree au module infra.remote.ftp")
finally:
return connected
def close(self):
""" Ferme la connexion passee en argument. """
logging.info("[+] Fermeture de la connexion a {}@{}".format(self.username, self.hostname))
if self.remote:
self.remote.close()
def get_file(self, remote_file, local_filepath, create_dir=False):
"""
Recupere le fichier distant 'filepath'.
Si create_dir est positionne a True et si le fichier 'filepath' est un chemin dote de plusieurs composantes (ex: path/to/file), alors l'arbroescence est recree localement dans le repertoire 'local_filepath'.
"""
local_fname = os.path.join(local_filepath, os.path.basename(remote_file))
if create_dir:
self.create_local_dirs(remote_file, local_filepath)
local_fname = os.path.join(local_filepath, remote_file)
logging.info("[+] Recuperation du fichier: {}".format(remote_file))
logging.info(" '-> {}".format(local_fname))
try:
if self.tls:
logging.debug(" '-> Negotiation de la taille du buffer securise")
resp = self.remote.sendcmd('PBSZ 0')
logging.debug(" '-> {}".format(resp))
try:
logging.debug(" '-> Passage en mode texte clair")
resp = self.remote.prot_c()
logging.debug(" '-> {}".format(resp))
except Exception as e:
logging.warning("[{}] {}".format(e.__class__.__name__, str(e)))
logging.debug(" '-> Chiffrement de la connexion")
resp = self.remote.prot_p()
logging.debug(" '-> {}".format(resp))
logging.debug(" '-> Passage en mode binaire")
resp = self.remote.sendcmd('TYPE I')
logging.debug(" '-> {}".format(resp))
logging.debug(" '-> Passage en mode passif")
resp = self.remote.sendcmd('PASV')
logging.debug(" '-> {}".format(resp))
if self.tls:
logging.debug(" '-> Passage en mode securise")
resp = self.remote.prot_p()
logging.debug(" '-> {}".format(resp))
logging.debug(" '-> Recuperation des donnees")
self.remote.retrbinary("RETR {}".format(remote_file), open(local_fname, 'wb').write)
except Exception as e:
logging.critical(" '-> [{}] {}".format(e.__class__.__name__, e))
sys.exit(1)
def put_file(self, filepath, remote_dir='', create_dir=False):
"""
Depose le fichier local 'filepath' sur le serveur distant dans le repertoire 'remote_dir'.
Si 'create_dir' est positionnee a True alors l'arborescence sera recree
sur le serveur distant.
"""
if remote_dir[0] == "/":
remote_dir = remote_dir[1:]
remote_fname = os.path.join(remote_dir, os.path.basename(filepath))
if create_dir:
self.create_remote_dirs(filepath)
remote_fname = os.path.join(remote_dir, filepath)
logging.info("[+] Televersement du fichier: {}".format(filepath))
logging.info(" '-> {}".format(remote_fname))
try:
if self.tls:
logging.debug(" '-> Negotiation de la taille du buffer securise")
self.remote.sendcmd('PBSZ 0')
try:
logging.debug(" '-> Passage en mode texte clair")
resp = self.remote.prot_c()
logging.debug(" '-> {}".format(resp))
except Exception as e:
logging.warning("[{}] {}".format(e.__class__.__name__, str(e)))
logging.debug(" '-> Chiffrement de la connexion")
resp = self.remote.prot_p()
logging.debug(" '-> {}".format(resp))
logging.debug(" '-> Passage en mode binaire")
self.remote.sendcmd('TYPE I')
logging.debug(" '-> Passage en mode passif")
self.remote.sendcmd('PASV')
if self.tls:
logging.debug(" '-> Passage en mode securise")
self.remote.prot_p()
logging.debug(" '-> Envoi des donnees")
self.remote.storbinary('STOR {}'.format(remote_fname), open(filepath, 'rb'))
except Exception as e:
logging.critical("Erreur lors du transfert: [{}]: {}".format(e.__class__.__name__, e))
sys.exit(1)
def create_local_dirs(self, remote_file, local_filepath):
"""
Creation de l'arborescence de 'remote_file' dans le repertoire local_filepath.
La derniere composante de 'remote_file' est consideree comme un fichier.
"""
hierarchy = remote_file.split('/')[:-1]
logging.info("[+] Creation de l'arborescence locale:")
for component in hierarchy:
local_file_path = os.path.join(local_filepath, component)
logging.info(" '-> {}".format(local_file_path))
try:
if not os.path.isdir(local_file_path):
os.mkdir(local_file_path)
except Exception as e:
logging.critical(" '-> [{}] {}".format(e.__class__.__name__, e))
sys.exit(1)
def create_remote_dirs(self, remote_file, remote_dir=''):
"""
Creation de l'arborescence de 'remote_file' sur l'hote distant a partir de 'remote_dir'.
La derniere composante de 'remote_file' est consideree comme un fichier.
"""
hierarchy = remote_file.split('/')[:-1]
logging.info("[+] Creation de l'arborescence distante:")
for component in hierarchy:
remote_dir = os.path.join(remote_dir, component)
logging.info(" '-> {}".format(remote_dir))
try:
self.remote.mkd(remote_dir)
except Exception as e:
logging.critical(" '-> [{}] {}".format(e.__class__.__name__, e))
sys.exit(1)
def delete_remote_file(self, remote_file):
"""
Supprime le fichier distant dont le chemin complet est passe en argument.
"""
try:
self.remote.delete(remote_file)
except Exception as e:
logging.critical(" '-> [{}] {}".format(e.__class__.__name__, e))
sys.exit(1)
def delete_remote_dir(self, remote_dir):
"""
Supprime le repertoire distant dont le chemin complet est passe en argument.
"""
try:
self.remote.rmd(remote_dir)
except Exception as e:
logging.critical(" '-> [{}] {}".format(e.__class__.__name__, e))
sys.exit(1)
def list(self, remote_dir, file_type='f', pattern='.*'):
"""
Renvoie la liste des fichiers presents dans le repertoire remote_dir.
L'argument file_type permet de selectionner le type de fichier liste (f=file (valeur par defaut), d=directory).
"""
content = []
logging.info("[+] Recuperation du contenu du repertoire distant: {}".format(remote_dir))
try:
self.remote.retrlines('LIST {}'.format(remote_dir), content.append)
if file_type == 'f':
reg = re.compile("^-.*")
elif file_type == 'd':
reg = re.compile("^d.*")
content = [os.path.join(remote_dir, f.split()[-1]) for f in content if re.match(reg, f)]
reg = re.compile(pattern)
content = [os.path.join(remote_dir, f.split()[-1]) for f in content if re.match(reg, f)]
return content
except Exception as e:
logging.critical(" '-> [{}] {}".format(e.__class__.__name__, e))
sys.exit(1)

+ 0
- 548
infra/remote/gncftp.py View File

@ -1,548 +0,0 @@
"""
Réecriture de la classe FTP fournie par ftplib afin de permettre le FTPes
ainsi que le FTPS sur n'importe quel port TCP.
© 2020 - MCO System - https://www.mcos.nc
"""
import logging
import re
import socket
import ssl
from socket import _GLOBAL_DEFAULT_TIMEOUT
# ------------------------------------------------------------------------------
# Des contantes "touche pas à ça 'ptit con'©"
# ------------------------------------------------------------------------------
CRLF = '\r\n'
B_CRLF = b'\r\n'
MSG_OOB = 0x1
# ------------------------------------------------------------------------------
# Classes d'erreurs spécifique
# ------------------------------------------------------------------------------
class Error(Exception):
pass
class error_reply(Error):
pass
class error_temp(Error):
pass
class error_perm(Error):
pass
class error_proto(Error):
pass
# ------------------------------------------------------------------------------
# Des fonctions pour parser les réponses reçues
# ------------------------------------------------------------------------------
def sanitize(message):
if message[:5] in {'pass ', 'PASS '}:
i = len(message.rstrip('\r\n'))
message = message[:5] + '*' * (i - 5) + message[i:]
return repr(message)
def parse150(resp):
if resp[:3] != '150':
raise error_reply(resp)
reg_150 = re.compile(r"150 .* \((\d+) bytes\)", re.IGNORECASE | re.ASCII)
m = reg_150.match(resp)
if not m:
return None
return int(m.group(1))
def parse227(resp):
if resp[:3] != '227':
raise error_reply(resp)
reg_227 = re.compile(r'(\d+),(\d+),(\d+),(\d+),(\d+),(\d+)', re.ASCII)
m = reg_227.search(resp)
if not m:
raise error_proto(resp)
numbers = m.groups()
host = '.'.join(numbers[:4])
port = (int(numbers[4]) << 8) + int(numbers[5])
return host, port
def parse229(resp, peer):
if resp[:3] != '229':
raise error_reply(resp)
left = resp.find('(')
if left < 0:
raise error_proto(resp)
right = resp.find(')', left + 1)
if right < 0:
raise error_proto(resp)
if resp[left + 1] != resp[right - 1]:
raise error_proto(resp)
parts = resp[left + 1:right].split(resp[left + 1])
if len(parts) != 5:
raise error_proto(resp)
host = peer[0]
port = int(parts[3])
return host, port
def parse257(resp):
if resp[:3] != '257':
raise error_reply(resp)
if resp[3:5] != ' "':
return ''
dirname = ''
i = 5
n = len(resp)
while i < n:
c = resp[i]
i = i + 1
if c == '"':
if i >= n or resp[i] != '"':
break
i = i + 1
dirname = dirname + c
return dirname
def print_line(line):
print(line)
# ------------------------------------------------------------------------------
# Classe FTP à nous qu'on a
# ------------------------------------------------------------------------------
class GncFtp:
"""
Réimplémentation du protocole FTP car l'implémentation de référence Python
(ftplib) ne permet pas de sélectionner un port autre que 21 pour le FTPes.
Cette classe reprend la quasi intégralité de la classe d'origine.
"""
ENCODING = 'latin-1'
maxline = 8192
sock = None
af = None
file = None
welcome = None
passive = 1
ssl_version = None
context = None
_prot_p = False
def __init__(self, hostname, port=21,
username='anonymous', password='anonymous',
tls=True, explicit=True, timeout=_GLOBAL_DEFAULT_TIMEOUT):
self.hostname = hostname
self.port = port
self.username = username
self.password = password
self.tls = tls
self.explicit = explicit
self.timeout = timeout
if self.tls:
self.ssl_version = ssl.PROTOCOL_TLSv1_2
self.context = ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH)
def __enter__(self):
self.connect()
return self
def __exit__(self, type_err, value, traceback):
del type_err, value, traceback
self.close()
def connect(self):
logging.debug("[+] Connection")
if self.hostname is not None:
logging.debug(" '-> Socket creation")
self.sock = socket.create_connection((self.hostname, self.port),
self.timeout)
logging.debug(" '-> Register socket family")
self.af = self.sock.family
if self.tls:
logging.debug(" '-> TLS")
if self.explicit:
logging.debug(" '-> File creation from socket")
self.file = self.sock.makefile('r', encoding=self.ENCODING)
logging.debug("[WELCOME] {}".format(self._getresp()))
logging.debug(" '-> Explicit TLS")
self.auth()
else:
logging.debug(" '-> Implicit TLS")
self.sock = self.context.wrap_socket(self.sock,
server_hostname=self.hostname)
logging.debug(" '-> File creation from socket")
self.file = self.sock.makefile('r', encoding=self.ENCODING)
logging.debug("[WELCOME] {}".format(self._getresp()))
else:
self.file = self.sock.makefile('r', encoding=self.ENCODING)
logging.debug("[WELCOME] {}".format(self._getresp()))
logging.info("[=] connected")
return True
raise Error("Remote host not defined")
def auth(self):
if isinstance(self.sock, ssl.SSLSocket):
raise ValueError("Connection is already secured")
if self.ssl_version >= ssl.PROTOCOL_TLS:
resp = self.voidcmd('AUTH TLS')
else:
resp = self.voidcmd('AUTH SSL')
logging.debug(" '-> SSL Socket (explicit)")
self.sock = self.context.wrap_socket(self.sock,
server_hostname=self.hostname)
logging.debug(" '-> File creation from secure socket")
self.file = self.sock.makefile(mode='r', encoding=self.ENCODING)
return resp
def login(self):
resp = self.sendcmd('USER {}'.format(self.username))
if resp[0] == '3':
resp = self.sendcmd('PASS {}'.format(self.password))
if resp[0] != '2':
raise error_reply(resp)
logging.info("[=] Logged in")
return resp
def _putline(self, line):
if '\r' in line or '\n' in line:
raise ValueError('Illegal newline character should not be contained')
line = line + CRLF
self.sock.sendall(line.encode(self.ENCODING))