Etudier Linux depuis un container docker peut être intéressant pour apprendre en même temps Docker. Nous allons voir comme créer un container docker qui contient Debian, et nous allons commencer par nous familiariser avec Docker.
Téléchargement de l’image Docker officielle Debian
Pour télécharger l’image officielle de Debian faites la commande suivante (démarrer Docker Desktop avant)
docker pull debian
Démarrage de l’image docker debian
Une fois que l’image est téléchargée on peut démarrer le container docker
docker run -it debian
//alternativement démarrage avec un container nommé
docker run -it --name not-ubuntu debian
// voir les dockers en fonctionnement
// depuis un autre terminal, pour visualiser les docker qui tournent
docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
a3de0ee8e2ba debian "bash" 2 hours ago Up 54 minutes not-ubuntu
Sortir d’un docker
Pour sortir d’un docker quand vous êtes en mode interactif, c’est à dire que vous êtes dedans, vous pouvez faire la combinaison de touche CTRL + D, ou faire la commande exit.
Quand vous avez démarré votre container docker avec docker run, vous pouvez voir les container actif avec docker ps, mais un fois que vous êtes sorti du container ou que le container est stoppé, docker ps ne donne rien, c’est alors qu’il vous faut faire la commande docker ps -a
Stopper un container
Vous pouvez stopper un container à partir d’un autre terminal
docker stop <ID CONTAINER>
Une fois le container stoppé, il ne sera plus visible avec docker ps, mais avec docker ps -a.
docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
a3de0ee8e2ba debian "bash" 2 hours ago Exited (137) 5 minutes ago not-ubuntu
Redémarrer un container « Exited »
Pour redémarrer un container éteint, il faut faire docker start <nom_container>. Le problème c’est que vous ne pouvez pas le redémarrer en mode interactif. Il faut le redémarrer et y entrer avec docker exec.
docker start not-ubuntu
// entrée dans le docker avec un shell bash
docker exec -it not-ubuntu bash
Construire sa propre image docker de Debian
Pourquoi cette manipulation? L’image officielle de Debian ne contient que Debian. Si vous démarrez cette image, il faut tout installer (Proftpd, curl,wget etc). Nous voudrions une image qui contient tout ces logiciels. Bien sûr vous pouvez démarrer un container nommé afin de persister les installations. Mais votre image n’est pas distribuable car elle sera modifiée. Nous allons donc confectionner une image Docker de Debian customisée avec des logiciel préinstallés, un utilisateur créé. Qu’on va pusher sur le registry gratuit de Docker, qu’on pourra distribuer.
Créer un fichier Dockerfile de configuration
Créer un répertoire où vous allez mettre le fichier Dockerfile, dont voici le contenu:
# Utilise l’image officielle Debian
FROM debian:bullseye
# Évite les questions interactives lors des installations
ENV DEBIAN_FRONTEND=noninteractive
# Mise à jour et installation de paquets de base
RUN apt-get update && \
apt-get install -y \
curl \
wget \
vim \
git \
ca-certificates \
sudo \
bash \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
# Crée un utilisateur non-root
RUN useradd -ms /bin/bash devuser && \
echo "devuser ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
# Bascule sur l’utilisateur
USER devuser
# Répertoire de travail
WORKDIR /home/devuser
# Commande par défaut (ouvre un terminal interactif)
CMD [ "bash" ]
La commande apt-get clean permet d’effacer les fichier temporaire pour gagner de l’espace, vous aurez une image Docker plus petite.
Ensuite depuis el répertoire où il y a le Dockerfile lancez la commande :
docker build -t debian_custom .
debian_custom est le nom de la nouvelle image, le . est le répertoire où se trouve le fichier Dockerfile (répertoire courant)
Pour tester l'image nous allons démarrer le container:
docker run -it debian_custom
Normalement tout fonctionne normalement, et vous êtes connecté en devuser. Maintenant nous allons pousser cette image dans notre repository Docker, si vous n’avez pas de compte c’est le moment d’en créer un.
docker tag debian_custom refschool/debian_custom:latest
docker push refschool/debian_custom:latest
The push refers to repository [docker.io/refschool/debian_custom]
5f70bf18a086: Preparing
34d4f9b85dd7: Pushing [==================================================>] 350.7kB
e5f126b4d117: Preparing
46cd9334c732: Preparing
puis quand c'est terminé
5f70bf18a086: Mounted from library/wordpress
34d4f9b85dd7: Pushed
e5f126b4d117: Pushed
46cd9334c732: Mounted from library/debian
latest: digest: sha256:fa443c6962fb73b4863bb10c46e41aef3db2c255604babcf83d89d41f1fbbb size: 1155
Allez sur le site https://hub.docker.com/repositories/refschool pour voir votre image en ligne ou allez dans le Docker Desktop.
Divers commandes docker
Les images qui n’ont plus de container actif peuvent être enlevé si elles ne servent plus. Les volumes de containers stoppés ou enlevés, les caches de builds.
Enlever une image par le nom ou par l’ID
docker images
ebian_custom latest 48c43ff6050a 34 minutes ago 271MB
refschool/debian_custom latest 48c43ff6050a 34 minutes ago 271MB
debian latest b2ab84c007fe 4 weeks ago 117MB
<none> <none> 9f0461ec704d 3 months ago 53.3MB
<none> <none> eb50ff518c13 3 months ago 47.9MB
test latest d5aa53c2ffcd 3 months ago 47.9MB
evashen latest bc040446118f 3 months ago 53.3MB
hubproxy.docker.internal:5555/docker/desktop-kubernetes kubernetes-v1.27.2-cni-v1.2.0-critools-v1.27.0-cri-dockerd-v0.3.2-1-debian c763812a4530 24 months ago 418MB
registry.k8s.io/kube-apiserver v1.27.2 c5b13e4f7806 2 years ago 121MB
registry.k8s.io/kube-scheduler v1.27.2 89e70da428d2 2 years ago 58.4MB
registry.k8s.io/kube-controller-manager v1.27.2 ac2b7465ebba 2 years ago 112MB
registry.k8s.io/kube-proxy v1.27.2 b8aa50768fd6 2 years ago 71.1MB
docker/desktop-vpnkit-controller dc331cb22850be0cdd97c84a9cfecaf44a1afb6e 556098075b3d 2 years ago 36.2MB
registry.k8s.io/coredns/coredns v1.10.1 ead0a4a53df8 2 years ago 53.6MB
registry.k8s.io/etcd 3.5.7-0 86b6af7dd652 2 years ago 296MB
registry.k8s.io/pause 3.9 e6f181688397 2 years ago 744kB
docker/desktop-storage-provisioner v2.0 99f89471f470 4 years ago 41.9MB
docker rmi 9f0461ec704d // on enlève par l'ID
docker rmi evashen. // on enlève par le nom
Pour aller plus vite vous avez la commande:
docker image prune -a
// qui va tout enlever après confirmation
Pourquoi ce titre? Non seulement je voulais faire l’installation de Proftpd, mais je voulais aussi tester l’accès à ce serveur FTP logé dans WSL depuis l’hôte Windows. Pourquoi est ce que c’est intéressant? hé bien WSL est un milieu isolé de l’hôte et on ne peut y accéder que grâce à une translation d’adresse IP.
Installation de Proftpd
D’abord on fait un update de précaution et on installe, l aconfiguration devrait être minimale
sudo apt update
sudo apt install proftpd
// le fichier de configuration se trouve dans /etc/proftpd/proftpd.conf Voyons voir quelques clé de configuration:
ServerType standalone // proftpd fonctionne tout seul, il est autosuffisant
Ce bout de configuration est commenté
<Anonymous ~ftp>
User ftp
Group nogroup
UserAlias anonymous ftp
MaxClients 10
<Directory *>
<Limit WRITE>
DenyAll
</Limit>
</Directory>
</Anonymous>
Il permet de se connecter sans authentification, ce qui n'est pas recommandé à moins que vous ne sachiez ce que vous faites.
A un autre endroit (commenté également)
DefaultRoot ~ // si le tilde est collé à la lettre t c'est une erreur de syntaxe et cause le non démarrage de pProftpd
cette directive restreint les utlisateurs dans leur propre répertoire home.
RequireValidShell off // si actif permet aux utilisateur n'ayant pas de shell (nologin) d'accéder au FTP
Accès au serveur FTP depuis WSL
a présent sans configuration vous pouvez accéder via FTP à votre répertoire home avec la commande suivante:
ftp localhost
il va vous être demandé le user et le mot de passe. Et ensuite en cas de réussite vous aurez un prompt ftp.
Accès depuis l’hôte Windows dans le ftp de WSL
Translation d’adresse IP
Revenons à votre hôte Windows et essayez avec fileZilla de vous connecter au FTP, normalement vous ne pouvez pas, à moins que vous ayez déjà fait le manipulation de translation d’adresse IP, c’est à dire relier l’adresse IP de l’hôte (souvent 192.168.1.XX vers l’adresse IP de WSL. Commençons par connaitre les 2 adresse IP de ‘lhôte et de WSL
Dans l’hôte, on est sous Windows
ipconfig
Configuration IP de Windows
Carte Ethernet Ethernet 2 :
Suffixe DNS propre à la connexion. . . : lan
Adresse IPv4. . . . . . . . . . . . . .: 192.168.1.151
Masque de sous-réseau. . . . . . . . . : 255.255.255.0
Passerelle par défaut. . . . . . . . . : 192.168.1.254
Carte inconnue Connexion au réseau local :
Statut du média. . . . . . . . . . . . : Média déconnecté
Suffixe DNS propre à la connexion. . . :
Carte Ethernet vEthernet (Default Switch) :
Suffixe DNS propre à la connexion. . . :
Adresse IPv6 de liaison locale. . . . .: fe80::302e:b41d:9890:f78f%41
Adresse IPv4. . . . . . . . . . . . . .: 172.25.144.1
Masque de sous-réseau. . . . . . . . . : 255.255.240.0
Passerelle par défaut. . . . . . . . . :
Carte Ethernet vEthernet (WSL) :
Suffixe DNS propre à la connexion. . . :
Adresse IPv6 de liaison locale. . . . .: fe80::94b8:a9bc:705a:2d44%55
Adresse IPv4. . . . . . . . . . . . . .: 172.18.96.1
Masque de sous-réseau. . . . . . . . . : 255.255.240.0
Passerelle par défaut. . . . . . . . . :
Dans WSL
ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1404 qdisc mq state UP group default qlen 1000
link/ether 00:15:5d:d9:50:6a brd ff:ff:ff:ff:ff:ff
inet 172.18.104.160/20 brd 172.18.111.255 scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::215:5dff:fed9:506a/64 scope link
valid_lft forever preferred_lft forever
Maintenant dans Windows nous devons faire la translation d’IP (dans une autre occasion on a pu le faire mais je remets ici la commande à faire sous Powershell
Nous avons vu comment créer un serveur qui répond à des messages avec socat, lancé en ligne de commande, on va aller plus loin en mettant la ligne de lancement de socat dans un script shell, et en loggant les messages échangés
//server.sh
#!/bin/bash
mkdir -p logs
echo "Serveur socat en écoute sur le port 5000..."
socat TCP-LISTEN:5000,reuseaddr,fork EXEC:"$(pwd)/handler.sh"
//handler.sh
#!/bin/bash
timestamp=$(date +"%Y-%m-%d_%H-%M-%S")
client_ip=$SOCAT_PEERADDR
logfile="logs/client_${client_ip}_${timestamp}.log"
echo "Client $client_ip connecté à $timestamp" >> "$logfile"
echo "Bienvenue $client_ip ! Tape 'exit' pour quitter."
while read line; do
echo "[$(date +%H:%M:%S)] $client_ip: $line" >> "$logfile"
[ "$line" = "exit" ] && echo "Au revoir $client_ip !" && break
echo "Tu as dit : $line"
done
echo "Déconnexion de $client_ip" >> "$logfile"
Le fichier server.sh est presque inchangé, par contre le fichier de traitement handler.sh est plus complexe. Il consigne d’abord l’adresse IP du client qui servira a nommer le fichier de log, et utilise le timestamp dans le nom du fichier. Dans la boucle while, les messages entrés par le client sont redirigé vers le fichier de log en mode append (sans écrasement du contenu)
Allons plus loin avec un broadcast !
Le broadcast consiste à diffuser à tout le monde, on se rapproche du chat
Avec ce qui suit on aura :
Plusieurs clients peuvent se connecter.
Chacun reçoit tous les messages envoyés par les autres.
On utilise une file nommée (FIFO) pour centraliser les messages.
socat crée un processus par client.
Cette fois-ci on va se doter d’un named pipe
Strucutre des fichiers
home/refschool/
├── server.sh # Démarre le serveur socat
├── handler.sh # Gère chaque client
├── broadcast.fifo # File partagée pour les messages
└── logs/ # Logs par client
// server.sh
#!/bin/bash
mkdir -p logs
FIFO="broadcast.fifo"
[ -p "$FIFO" ] || mkfifo "$FIFO"
echo "Serveur de chat en écoute sur le port 5000..."
socat TCP-LISTEN:5000,reuseaddr,fork EXEC:"$(pwd)/handler.sh"
Serveur :
#!/bin/bash
timestamp=$(date +"%Y-%m-%d_%H-%M-%S")
client_ip=$SOCAT_PEERADDR
logfile="logs/client_${client_ip}_${timestamp}.log"
fifo="broadcast.fifo"
echo "Client $client_ip connecté à $timestamp" >> "$logfile"
# Démarrer un lecteur en arrière-plan qui lit la FIFO et envoie au client
tail -f "$fifo" &
# Lire les messages du client et les écrire dans la FIFO
while read line; do
[ "$line" = "exit" ] && echo "💬 $client_ip s'est déconnecté." >> "$fifo" && break
echo "[$(date +%H:%M:%S)] $client_ip: $line" >> "$logfile"
echo "💬 $client_ip: $line" >> "$fifo"
done
# Nettoyer le processus tail
kill %1 2>/dev/null
echo "Déconnexion de $client_ip" >> "$logfile"
Voilà avec ce script, tous les clients auront ce que les autres ont tapé dans l’invite. On peut utiliser ce système pour faire un jeu en réseau même simple (tic tac toe)
NodeJs est apparu avec les commonJs modules, puis après avec la grosse évolution de ES, qui a introduit les ES Module, est apparu une seconde façon de faire plus moderne. A quoi ressemble un module CommonJS?
Du point de vue d’un script NodeJS la syntaxe est la suivante
const express = require('express')
La syntaxe ES Module utilise le mot clé import
import Express from 'express'
Je rappelle qu’un module est un script Javascript. Dans le fichier package.json, il y a un champ qui dit quel système de module utiliser
Dans le script ci-dessus, type est associé à « module », alors que cela pourrait être « commonjs ».
En mode « commonjs », les fichier avec extension .cjs,.js sont traités comme module CommonJS, mais voyons cela dans un tableau récaitulatif
Extension de fichiers
Comportement en type= »commonjs »
Comportement en type= »module »
.cjs
CommonJS module
CommonJS module
.js
CommonJS module
ES Module
.mjs
ES Module
ES Module
On voit dans ce tableau que les extensions cjs et mjs sont invariable, et que c’est js qui peut changer de comportement suivant le contexte.
Règles d’application
Dans le contexte ES Module:
on import ou export dans ce contexte
// salutation.js
export function saluer(){
console.log("Bonjour !")
}
puis plus tard en utilisation dans un autre fichier index.js
import saluer from './salutation.js'
Dans le contexte CommonJS:
// salutation.js
function saluer(){
console.log("Bonjour !")
}
module.exports = saluer
puis plus tard en utilisation dans un autre fichier index.js
const saluer = require('./salutation.js')
Vous ne pouvez utiliser que import et export dans un ES Module, c’est à dire lorsque vous êtes dans un fichier mjs ou js avec type : « module » (Cannot use import statement outside a module, Unexpected token ‘export’)
Vous ne pouvez pas utiliser require dans un ES Module, vous devez utiliser import(ReferenceError: require is not defined)
Vous ne pouvez utiliser require pour charger un EX Module (Error [ERR_REQUIRE_ESM]: Must use import to load ES Module
Cela semble logique, quand on sait dans quel contexte on est, on joue avec les règles. Ainsi On ne peut mélanger les syntaxe des 2 contexte. MAIS il y a un cas où l’on peut mélanger (quoique je ne vous recommande pas d’utiliser expliciter, mais lorque vous êtes obligé seulement)
Un ES Module peut importer un module exporté depuis un contexte CommonJS
Pour la première démonstration de ce script on se mettra en wsl dans le répertoire /home/refschool pour faire fonctionner le script, car il faut veiller à ce qu’on soit dans un système de fichier Linux pour pouvoir créer un named pipe.
De plus on utilisera le tutoriel Tmux pour avoir un multifenêtrage dans une seule console.
Le script ci-dessus va effacer un named pipe appelé « pipe », et aussitôt en recréer un, puis entre dans une boucle infinie pour écouter sur le port 5000 du serveur netcat (nc)
On va ouvrir tmux et créer deux panneaux verticaux avec le raccourcis CTRL + B puis « % », et pour basculer d’un panneau à l’autre, utiliser le raccourcis CTRL +B puis « o », le premier panneau servira à exécuter le script shell, et le second servira à entrer des messages au serveur netcat.
Donc dans le premier panneau on va lancer le script shell, et on bascule vers le second panneau avec le raccourci CTRL + B puis « o », une fois dans le second panneau, on va se connecter au serveur netcat sur le port 5000
nc localhost 5000
// ensuite dans le même panneau entrez des textes
hello
bonjour
puis entrez exit qui sera interprété par le script shell comme une interruption du programme.
Dans ce qui a précédé, le serveur netcat écoutait sur la boucle locale localhost, ou 127.0.0.1 ou loopback. Mais un autre ordinateur sur le même réseau LAN ou WIFI ne peut pas s’y connecter. Pour ce faire il faudrait démarrer netcat pour écouter sur 0.0.0.0 (tous les interfaces du LAN), mais pas d’Internet bien sûr et ce pour plusieurs raisons :
votre ordinateur est derrière un routeur (votre box si vous êtes chez vous), il faut utiliser le port forwarding
il se peut aussi que le firewall de Windows bloque le port 5000 (hypothétique)
Vous pouvez utiliser un tunnel pour créer une connexion avec le monde extérieur.
Ecouter les messages d’ordinateur du même réseau domestique
Votre serveur netcat va démarrer sur votre poste, donc il est utile de connaitre son adresse IP dans le LAN avec cette commande
ip addr show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1404 qdisc mq state UP group default qlen 1000
link/ether 00:15:5d:d9:5f:23 brd ff:ff:ff:ff:ff:ff
inet 172.18.104.160/20 brd 172.18.111.255 scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::215:5dff:fed9:5f23/64 scope link
valid_lft forever preferred_lft forever
Remarquez que l’adresse IP affichée ici (en rapport avec eth0) es 172.18.104.160, vous vous attendez à une adresse du type 192.168.1.xx, c’est parce que on est à l’intérieur de WSL, et que cette adresse est interne à WSL.
Pour connaitre l’adresse IP de l’hôte avec ipconfig:
C:\Users\admin>ipconfig
Configuration IP de Windows
Carte Ethernet Ethernet 2 :
Suffixe DNS propre à la connexion. . . : lan
Adresse IPv4. . . . . . . . . . . . . .: 192.168.1.151
Masque de sous-réseau. . . . . . . . . : 255.255.255.0
Passerelle par défaut. . . . . . . . . : 192.168.1.254
Carte inconnue Connexion au réseau local :
Statut du média. . . . . . . . . . . . : Média déconnecté
Suffixe DNS propre à la connexion. . . :
Carte Ethernet vEthernet (Default Switch) :
Suffixe DNS propre à la connexion. . . :
Adresse IPv6 de liaison locale. . . . .: fe80::302e:b41d:9890:f78f%41
Adresse IPv4. . . . . . . . . . . . . .: 172.25.144.1
Masque de sous-réseau. . . . . . . . . : 255.255.240.0
Passerelle par défaut. . . . . . . . . :
Carte Ethernet vEthernet (WSL) :
Suffixe DNS propre à la connexion. . . :
Adresse IPv6 de liaison locale. . . . .: fe80::94b8:a9bc:705a:2d44%55
Adresse IPv4. . . . . . . . . . . . . .: 172.18.96.1
Masque de sous-réseau. . . . . . . . . : 255.255.240.0
Passerelle par défaut. . . . . . . . . :
Maintenant nous allons corriger le script shell pour que netcat écoute sur toutes les interfaces, j’appelle ce fichier
A la ligne 15, nc (netcat) écoute sur toutes les interfaces, donc les ordinateurs du réseau local vont pouvoir communiquer avec mon serveur nc
Je vais utiliser mon Macbook pour faire la commande nc pour me connecter au serveur netcat.
nc 172.18.104.160 5000
mais rien ne se passe !
Souvenez vous que 172.18.104.160 est une adresse IP de WSL, ce n’est pas l’hôte (Windows) qui a l’adresse 192.168.1.151. Donc il faut que le Macbook se connect à cette dernière adresse. Mais ce n’est pas tout, le Macbook se connectant à 192.168.1.151, il faut que le message parviennet au 172.18.104.160. On obtient cette redirect par ce qu’on appelle la redirection de port, port forwarding de Windows à WSL. Pour ce faire il faut s’aider de Powershell en mode administrateur
Je vous conseille de redémarrer le script shell sous WSL et de tester d’abord en local puis depuis le Mac. Si vous ne faites pas le redémarrage, ça ne marche pas.
Pour confirmer le port forwarding sous Windows
netsh interface portproxy show all
Listen on IPv4: Connect to IPv4:
Address Port Address Port
--------------- ---------- --------------- ----------
0.0.0.0 5000 172.18.104.160 5000
En mpeme temps depuis votre WSL confirmez que le script shell écoute aussi sur le port 5000
Normalement ça marche depuis le Macbook maintenant ! Mais si nous essayons en même temps de le faire depuis un autre panneau de WSL ça ne marche pas. Par contre coupez toutes les connexions, puis commencez sur WSL ça marche ! mais ensuite vous basculez sur Macbook ça ne marche pas !
Limites de nc et socat à la rescousse
Hé oui nc n’écoute qu’une seule communication à la fois ! nc -l est en mono connexion, il accepte une connexion puis se ferme. Il faudrait une boucle infinie qui lance en continue nc du type
while true; do
nc -l -p 5000 -s 0.0.0.0
done
Mais il n’y a pas de parallélisme véritable, c’es séquentiel, donc un peu de lag au final.
Pour avoir du vrai parallélisme, il faut utiliser socat pour faire ce boulot. Donc on va l’installer
sudo apt install socat
// fonctionnement de socat
socat lance un fichier que vous désignez qui va prendre en charge les requêtes, un peu comme >> node server.js
La manipulation consiste à écrire un script shell qui va lire en entrée et faire quelque chose avec ce qui est lu,voici le script handler.sh
#!/bin/bash
echo "Bienvenue sur le serveur !"
while read line; do
echo "Tu as dit : $line"
done
Ensuite nous allons lancer le serveur socat :
socat TCP-LISTEN:5000,reuseaddr,fork EXEC:./handler.sh
En une ligne nous lançons le serveur socat, qui grâce aux paramétrage précédent de Windows (Firewall et translation d’adresse IP) va écouter sur tout le réseau local, depuis le Macbook, je lance
nc 192.168.1.151 5000
bienvenue sur le serveur !
Et on peut envoyer des messages au serveru socat depuis plusieurs clients !
On peut pousser plus loin la chose en mettant la ligne de lancement de socat dans un fichier shell, et consigner les messages dans un fichier de log personnalisé à chaque client.
vous avez entendu parler de machine learning partout, mais avez vous vu comment cela fonctionnait? Nous allons voir avec un exemple comment ça marcce à quoi ça sert et vous comprendrez à la fin pourquoi il est très intéressant d’en faire dans les entreprises.
Le contexte : avec des données d’abandon de souscription à un forfait mobile (churn), nous allons établir un modèle mathématique pour prédire les futur churn. L’intérêt de faire confiance au machine learning est qu’il y a un grand nombre de paramètres qui influence le fait qu’un client part à la concurrence, il est quasi impossible de faire le diagnostic à la main pour savoir si le client va partir ou non, plus exactement il y a 18 paramètres.
Le principe du machine learning est de prendre ces données, les travailler et les faire passer dans un algorithme qui va générer un modèle mathématique qui servira aux futures données, pour prédire si le client va churner ou non. En gros se servir du passé pour prédire le futur.
Le dataset à télécharger
Allez sur Kaggle et téléchargez le dataset (il vous faudra créer un compte). On va explorer ce dataset, mais ce qu’il faut savoir c’est que dans ce dataset, 30% des lignes concernent les clients ayant churné, et 70% sont des clients qui n’ont pas churné. On n’a pas une distribution 50/50. En machine learning, entrainer sur ce dataset va produire un modèle mathématique qui va prédire de façon biaisé, quelquesoit l’entrée donnée, les probabilités que la prédiction soit en faveur d’un non churn sera anormalement haute.
Pour éviter ce déséquilibre dans les données (et ce déséquilibre importe car le type d’issue d’une prédiction est unique : le churn), on va créer artificiellement (oui artificiellement) des données pour compenser ce déséquilibre, donc on va générer des données ayant pour issue le churn, afin d’avoir un rapport 50/50 (churn/non-churn)
Installation des paquets
Pour faire du traitement de machine learning, il vous faudra installer les paquets suivants : numpy, pandas, matplotlib, seaborn,sklearn
Note: lors de l’installation de sklearn, vous aurez une erreur de paquet déprécié, en fait dans l’import de bibliothèque, c’est bien sklearn qu’il faut importer, mais lors que vous installez le paquet avec pip, c’est scikit-learn qu’il faut installer ! (Lien pour en savoir plus)
Utiliser l’éditeur de code Jupyter Notebook
Jupyter noteboopk est un éditeur de code progressif, très adapté à des non codeur pur et dur. Pour les datascientistes, c’est très pertinent car ils peuvent voir comment évolue leur traitement de données. Et pour tout le monde, c’est très visuel lors de l’apprentissage du machine learning, même pour un codeur pur et dur comme moi.
Vous pouvez décomposer le code en étape et refaire exécuter une étape à tout moment, pratique si vous vous trompez dans votre code et qu’il y a une erreur.
Pour installer Jupyter Notebook, faites la commande
# installation de jupyterlab
pip install jupyterlab
pour les macs
pip3 install jupyterlab
pour démarrer
jupyter lab
pip install notebook
ou pour les mac
pip3 install notebook
puis démarrer en faisant :
jupyter notebook
Note sur Jupyterlab : jupyterlab est la version moderne de jupyter notebook, et possède un système d eplugin plus simple à utiliser et un terminal et un éditeur de texte.
Commençons à explorer les caractéristiques de notre jeu de données
Notre objectif est d’entrainer un modèle sur le jeu de données, pour ensuite prédire la probabilité d’un client à passer à la concurrencer (churner). customerChurn.csv est le fichier de chez Kaggle.
import pandas as pd
df = pd.read_csv('customerChurn.csv')
df.head()
La commande df.head() affiche les 5 premières ligne du dataframe. Le dataframe est la version de Pandas d’un fichier Excel en gros.
Nous avons un fichier avec des colonnes, qu’on appelle de feature, puisque chaque colonne est une caractéristique d’un client. Pour voir le nom et le type de la donnée stockée dans chaque colonne ainsi que le nombre de colonnes, on fait
Et pour savoir combien il y a d’enregistrements on fait:
df["Churn"].value_counts()
Churn
No 5174
Yes 1869
Name: count, dtype: int64
donc on a 19 colonnes utile et une colonne inutile d’un point de vue machine learning, c’est la colonne customerID, elle ne comporte aucune information métier. La dernière colonne appelée Churn, prend deux valeuer, elle indique si le client est parti chez le concurrent ou non. Elle prend les valeur No ou Yes. Pour afficher le compte on fait:
df["Churn"].values_count()
Churn
No 5174
Yes 1869
Name: count, dtype: int64
il y a 1869 personnes partie, on note ici que le nombre de Yes et de No est différent dans un rapport 1 à 3 environ. Cela on le verra plus tard aura son importance dans l'entrainement du modèle. Il y a inégalité dans les 2 camps, cette disparité va avoir une incidence sur la qualité du modèle entrainé. En effet, le modèle résultant va être biaisé avec le No, car ce dernier est majoritaire. On devra équilibrer artificiellement ce dataset pour avoir un bon modèle de prédiction.
Analyse exploratoire des données
Maintenant explorons les relations entre les différentes catégorie et la colonne Churn
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
cols = ['gender','SeniorCitizen',"Partner","Dependents"]
numerical = cols
plt.figure(figsize=(20,4))
for i, col in enumerate(numerical):
ax = plt.subplot(1, len(numerical), i+1)
unique_vals = df[col].nunique()
sns.countplot(x=col, hue=col, data=df, legend=False,
palette=sns.color_palette("husl", df[col].nunique()))
#sns.countplot(x=str(col), data=df, palette=sns.color_palette("husl", unique_vals))
ax.set_title(f"{col}")
L’idée est ici de graphiquement regarder pour chaque colonne la distribution des valeurs. Ici on voit la colonne Genre, SeniorCitizen, Partner, Dependents, des varaible catégorielle relative à la démographie.
On voit qu’il y a autant d’homme que de femmes, mais que les seniors sont minoritaires, qu’il y a autant de célibataires que de mariés, et il y a 1 tier de la population qui est dépendantes.
Maintenant regardons la relation entre le MonthlyCharges et le Churn, on peut se dire que plus le forfait est élevé plus les gens ont tendance à partir
Effectivement c’est ce que nous constatons, les Yes sont entre 50 et 100 dollars / mois.
Pour comprendre le boxplot je vous joins ce petit schéma pris sur datatab.net
Maintenant analysons la relation entre quelques autres variable catégorielle avec le Churn
cols = ['InternetService',"TechSupport","OnlineBackup","Contract"]
plt.figure(figsize=(14,4))
for i, col in enumerate(cols):
ax = plt.subplot(1, len(cols), i+1)
sns.countplot(x ="Churn", hue = str(col), data = df)
ax.set_title(f"{col}")
Cette fois-ci les catégories ont plusieurs valeurs (3). On peut voir que pour InternetService, donc le forfait box de l’abonnement, les churners n’ont pas de forfait box ou très peu, idem pour le service ADSL, par contre ils sont nombreux à être sur fibre optique. Concernant les non churners, ils sont plus équitablement distribué.
InternetService : Il ressort clairement du graphique ci-dessus que les clients utilisant une connexion Internet par fibre optique résilient leur abonnement plus souvent que les autres. Cela pourrait s’expliquer par le fait que la fibre est un service plus coûteux, ou bien que ce fournisseur n’offre pas une bonne couverture.
TechSupport : De nombreux utilisateurs ayant résilié leur abonnement ne s’étaient pas inscrits au service d’assistance technique. Cela pourrait signifier qu’ils n’ont reçu aucune aide pour résoudre leurs problèmes techniques et ont décidé d’arrêter d’utiliser le service.
OnlineBackup : Beaucoup de clients ayant résilié leur abonnement ne s’étaient pas inscrits au service de sauvegarde en ligne pour le stockage de données.
Contract : Les utilisateurs qui ont résilié leur abonnement étaient presque toujours sous contrat mensuel. Cela semble logique, car ces clients paient au mois et peuvent facilement annuler leur abonnement avant le prochain cycle de paiement.
Même sans construire un modèle d’apprentissage automatique sophistiqué, une simple analyse basée sur les données comme celle-ci peut aider les organisations à comprendre pourquoi elles perdent des clients et ce qu’elles peuvent faire pour y remédier.
Par exemple, si l’entreprise se rend compte que la majorité des clients qui résilient leur abonnement ne se sont pas inscrits au service d’assistance technique, elle pourrait inclure ce service gratuitement dans certaines de ses futures offres afin d’éviter que d’autres clients ne partent.
Beaucoup de données sont de type Object, des valeurs non numériques, « TotalCharges » qui est un montant est un objet par exemple, on va le convertir en valeur numérique
Les valeurs catégorielles (non numérique) doivent être convertie en valeur numérique pour être modélisée, des colonnes avec pour valeur Yes/No, on peut rendre le Yes en 1 et le No en 0. Avec Sckitlearn on va pouvoir transformer ces valeurs en nombres. Si on regarde le dataframe sans les valeur numériques
La commande ci-dessus nous permet de voir les valeurs catégorielles des features. Transformaons les en valuer numérique avec ScikitLearn
from sklearn import preprocessing
le = preprocessing.LabelEncoder()
df_cat = cat_features.apply(le.fit_transform)
df_cat.head(1)
Scikitlearn convertit automatiquement les valeurs catégorielle en valeur numérique. Il n’y a pas de règle particulière, la valeur 0 correspond à la première valeur catégorielle ordonnées dans l’ordre alphabétique. No est avant Yes donc No vaut zéro et Yes vaut 1.
On avait dit que le Churn Yes était minoritaire dans les données, alors qu’il devait être à 50/50 avec le churn No.
Comme mentionné précédemment, le jeu de données est déséquilibré, ce qui signifie que la majorité des valeurs de la variable cible appartiennent à une seule classe. La plupart des clients du jeu de données n’ont pas résilié leur abonnement — seulement 27 % l’ont fait.
Ce problème de déséquilibre des classes peut entraîner de mauvaises performances d’un modèle d’apprentissage automatique. Certains algorithmes, lorsqu’ils sont entraînés sur un jeu de données déséquilibré, finissent toujours par prédire la classe majoritaire. Dans notre cas, par exemple, le modèle pourrait prédire qu’aucun client n’a résilié. Bien qu’un tel modèle semble très précis (il serait correct dans 73 % des cas), il ne nous est d’aucune utilité puisqu’il prédit toujours le même résultat.
Il existe différentes techniques pour résoudre le problème de déséquilibre des classes en apprentissage automatique. Dans ce tutoriel, nous allons utiliser une technique appelée suréchantillonnage (oversampling). Ce processus consiste à sélectionner aléatoirement des échantillons de la classe minoritaire et à les ajouter au jeu de données d’entraînement. Nous allons suréchantillonner la classe minoritaire jusqu’à ce que le nombre d’exemples soit égal à celui de la classe majoritaire.
Avant de procéder au suréchantillonnage, effectuons une séparation entre le jeu d’entraînement et le jeu de test. Nous appliquerons le suréchantillonnage uniquement sur le jeu d’entraînement, car le jeu de test doit rester représentatif de la population réelle.
On va constituer un dataset pour le training du modèle, et un dataset de test qu’on va soumettre qu modèle entrainé.
from sklearn.model_selection import train_test_split
finaldf = finaldf.dropna()
finaldf = finaldf.drop(['customerID'],axis=1)
X = finaldf.drop(['Churn'],axis=1)
y = finaldf['Churn']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33, random_state=42)
Chaque dataset est en deux parties X (majuscule) et y (minuscule), y représente la dernière colonne qui est le churn. Ainsi Dans le dataset de training, on a X_train et y_train. Et pour le dataset de test on a X_test et y_test.
C’est avec la librairie imblearn qu’on va construire le suréchantillon sur les données de training
from imblearn.over_sampling import SMOTE
oversample = SMOTE(k_neighbors=5)
X_smote, y_smote = oversample.fit_resample(X_train, y_train)
X_train, y_train = X_smote, y_smote
Ensuite consmptez le nombre de rangées :
y_train.value_counts()
Churn
0 3452
1 3452
Name: count, dtype: int64
On est à 50/50
Construction du modèle de prédiction du churn
Maintenant la partie vraiment importante ! On a passé beaucoup de temps pour nettoyer, changé la valeur catégorielle en valeur numérique, rééquilibré les population, maintenant on va entrainer le modèle !
from sklearn.ensemble import RandomForestClassifier
rf = RandomForestClassifier(random_state=46)
rf.fit(X_train,y_train)
Evaluation du modèle
On va évaluer l aprécision du modèle avec les données de test (dont on connait l’issue car ce sont les données d’origine)
#customer churn model evaluation
from sklearn.metrics import accuracy_score
preds = rf.predict(X_test)
print(accuracy_score(preds,y_test))
0.7733735458853942
On a une précision de 77%
Sauvegarder le modèle
On en va pas à chaque fois réentrainer le modèle quand on va faire une prédiction, donc on va le sauvegarder pour pouvoir le réutiliser.
#save the model
import joblib
# After training your model (format Pickle)
joblib.dump(rf, 'random_forest_model.pkl')
Sauvergarder les données de test et de training
#save training and testing data to file
import pickle
# Save X data
with open('X_train.pkl', 'wb') as f:
pickle.dump(X_train, f)
with open('X_test.pkl', 'wb') as f:
pickle.dump(X_test, f)
# Save y data
with open('y_train.pkl', 'wb') as f:
pickle.dump(y_train, f)
with open('y_test.pkl', 'wb') as f:
pickle.dump(y_test, f)
Chargement du modèle et des données sauvegardées
On va ouvrir un autre fichier Notebook vierge et on va juste charge le modèle et les données pour passer au crible. En effet le modèle que vous avez calculé va être réutilisé, et il n’est pas question de le calculer à chaque fois que vous avez besoin de scorer la probabilité d’un client de patir.
Chargement du modèle
import joblib
#load the model
loaded_rf = joblib.load('random_forest_model.pkl')
print(loaded_rf)
# Now you can use the loaded model to make predictions
#prediction = loaded_rf.predict(new_customer)
#print(prediction)
Chargement des données
#load the datas
import pickle
with open('X_test.pkl', 'rb') as f:
X_test = pickle.load(f)
with open('y_test.pkl', 'rb') as f:
y_test = pickle.load(f)
X_test.head()
On rejoue le test, X_test contient les clients, et on passe en argument de la fonction predict qui va nous retourner la variable prediction, qui est une Serie, contenant des 0 et 1, 0 correspondant à « No » et 1 à « Yes ».
# Now you can use the loaded model to make predictions
from sklearn.metrics import accuracy_score
prediction = loaded_rf.predict(X_test)
#la série qui contient les outcome (churn si 1 , 0 si no churn)
print(prediction)
[0 0 1 ... 0 1 1]
Ce qu'on voit c'est la colonne churn
Test avec un sous ensemble
Ici je simule le fait que j’ai un nouveau client à scorer, c’est donc une ligne (un seul client), je passe cette ligne en argument de la fonction predict, qui va nous retourner un Serie avec un seul élément (on n’a scoré qu’un client), ici on obtient un « Yes ».
# test on row
# subset = X_test[X_test['TotalCharges']<100]
# extract a line from X_test
oneline = X_test.loc[[6125]]
#convert to dataframe
#oneline = oneline.to_frame()
import pandas as pd
isinstance(oneline,pd.DataFrame) # test if it's DataFrame
print(oneline)
oneline.head()
prediction = loaded_rf.predict(oneline)
print(prediction)
[1] // Churn
Récemment j’ai rouvert mon Pycharm après quasiment 2 ans d’inactivité, et en plus j’ai mis à jour la version de Pycharm de la version 2020 à 2024. Toutes les dépendances ont sauté, impossible d’exécuter les programme, il y avait aussi un problème de chemin d’interpréteur auquel je ne faisait pas attention. Désinstaller et réinstaller les module ne résouds pas le problème, il faut détruire et reconstruire le venv, qui est essentiellement un environnement de gestion des dépendances.
Qu’est ce qu’un environnement virtuel?
C’est un répertoire qui contient les librairies que votre projet va utiliser. Cet environnement est spécifique à votre projet. Il contient des exécutables pour les opérations de maintenance et utilitaires. Si vous deviez reproduire le projet sur un autre ordinateur, il faut faire un fichier requirements.txt qui contiendra les références à toutes les librairie installées, en NodeJS on a package.json, en PHP on a composer.json.
Détruire l’environnement virtuel
Il suffit simplement d’effacer le répertoire venv.
Opération sur les package et l’environnement virtuel
Cela peut être un peu compliqué à gérer surtout quand vous avez des problème de charge de paquets (ou dépendances). Il faut savoir qu’il y a plusieurs installations de Python, dans le venv, dans un coin de votre disque dur, etc.
Localisation des paquets globaux
pip list -v # pour lister
Activation de l’environnement virtuel
Il faut exécuter un fichier activate.bat (windows) qui se trouve dans le répertoire venv/scripts
tapez activate.bat ou activate sous windows
dans Linux
source venv/bin/activate
une fois activé, le prompt est visible
(venv) $
Si l’environnement n’est pas activé, vous ne pourrez pas accéder à pip indirectement, il faudra indiquer le chemin jusqu’à lui.
Lister les package dans le fichier requirements
pip freeze > requirements
Installer ou mettre à jour les paquets depuis le fichier requirements.txt
L’ajout de Hardhat se fait comme pour tout ajout de blockchain.
Vérifiez que vous êtes bien connecté sur la blockchain locale, ensuite ajoutez un compte à votre wallet, à partir de la clé privée, il y a 20 comptes qui sont listés lorsque vous déployez le smart contract
On arrive sur cet écran :
On peut faire un transfert de token vers une autre addresse
Hardhat est un ensemble logiciciel pour vous aider à développer un projet blockchain, tout comme un Laragon ou MAMP va vous simuler un Internet local, Hardhat va vous simuler une blockchain locale, élément nécessaire pour faire marcher un smart contract.
Hardhat est à privilégier pour tout nouveau projet blockchain, car Truffle n’est plus maintenu, il est possible que beaucoup de projet legacy utilisent encore Truffle cependant.
Configuration de VSCode pour un projet blockchain
Installation du plugin Remix Light et Solidity
Le plugin Solidity permet le syntax highlighting et l’intégration avec le compilateur, Solidity est un langage compilé.
Remix Light permet de tester et débugger.
Structure du projet blockchain
Créez un répertoire my-solidity-project entrez dedans. La strucutre sera comme ci-dessous
my-solidity-project/
├── contracts/ # Your smart contracts go here
├── scripts/ # Deployment and interaction scripts
├── test/ # Test files for your contracts
├── hardhat.config.js # Hardhat configuration
└── package.json # Project dependencies
Nous allons avoir besoin de NPM pour développer un projet blockchain.
// initialisation d'une projet nodeJS avec le flag -y pour répondre oui à toutes les questions
npm init -y
Installations du framework Hardhat
npm install --save-dev hardhat @nomicfoundation/hardhat-ethers ethers dotenv @nomicfoundation/hardhat-toolbox
// initialisation du framework
npx hardhat (et suivre les instructions /!\ c'est NPX et non NPM)
// on va choisir create a javascript project (mais il faut savoir que Typescript est très populaire)
Création du premier contrat
Un contrat est simplement un fichier en langage Solidity
Codage d’un contrat en Solidity MyToken
Rappel vous devez avoir la structure suivante:
my-solidity-project/
├── contracts/ # Your smart contracts go here
├── scripts/ # Deployment and interaction scripts
├── test/ # Test files for your contracts
├── hardhat.config.js # Hardhat configuration
└── package.json # Project dependencies
// le fichier se trouve dans contracts/MyToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract MyToken is ERC20, Ownable {
constructor(
address initialOwner
) ERC20("MyToken", "MTK") Ownable(initialOwner) {
_mint(msg.sender, 1000000 * 10 ** decimals());
}
function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
}
}
Le script ci-dessus fait appel à OpenZeppelin, on va installer ce dernier
// tests/myToken.js
const { expect } = require("chai");
describe("MyToken", function () {
let myToken;
const initialMessage = "Hello, Hardhat!";
beforeEach(async function () {
const MyToken = await ethers.getContractFactory("MyToken");
myToken = await MyToken.deploy(initialMessage);
});
it("Should return the initial message", async function () {
expect(await myToken.getMessage()).to.equal(initialMessage);
});
it("Should set a new message", async function () {
const newMessage = "New message";
await myToken.setMessage(newMessage);
expect(await myToken.getMessage()).to.equal(newMessage);
});
});
Compilation du contrat
// compilation des contracts
npx hardhat compile
// les fichiers compilés sont dans artifacts/contracts
// jouer les test
npx hardhat test
Si vous avez une erreur essayez la commande suivante:
npm cache clean --force
Démarrer la blockchain locale
Tout comme en Web2 on a un internet local avec un serveur web, nous allons lancer une blockchain locale. Ensuite on va pouvoir déployer notre smart contract.
// démarrer le noeud de blockchain local
npx hardhat node
// cette commande va afficher les 20 wallets créés par défaut
Déployer le contrat
// Déployer le contract dans le noeud local
npx hardhat run scripts/deploy.js --network localhost
Deploying contracts with the account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
MyToken deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
Cette étape est importante car vous allez voir l’adresse de votre smart contract
En résumé :
compiler le smart contract
Tester le smart contract
démarrer un noeud
déployer les smart contrat sur le noeud
interagir avec le smart contract
Et voilà !
Interagir avec un smart contract qu’on a déployé
Démarrez plusieurs terminaux pour avoir la vue sur tout les scripts du projet. Nous allons interagir avec le smart contract. Pas question encore de le faire avec un wallet du genre Metamask, il y a plusieurs façons d’interagir avec un smart contract.
Interaction avec une console Hardhat
//démarre une console Hardhat et la branche sur le réseau localhost
npx hardhat console --network localhost
Ensuite il y a une invite de commande. Vous allez mettre ce code dans l’invite de commande. Il n’y a pas de fichier javascript, on interagit en ligne de commande. Pour utiliser les exemples, il faut remplacer l’adresse du smart contract par celui que vous obtenez au lancement.
Parallel nous permet de faire tourner un programme simultanément sur plusieurs coeurs. Comme aujourd’hui tous les ordinateur on t des chips multicoeur, il ne faudrait pas s’en priver si nous avons des tâches intensives en calcul à faire.
Pour installer parallel :
sudo apt install parallel
Parallel exploite le multicoeur de votre système, pour connaitre le nombre de coeurs physique de votre machine :
$ lscpu
Autre méthode comme tout est fichier dans linux, dans le répertoire proc
vim /proc/cpuinfo
La commande nproc affiche des informations plusconcises
$ nproc
4
Rappel : les process, jobs, avant plan (foreground) et arrière plan (background)
Le signe & pour mettre en arrière plan
ping google.com > ping.txt &
[1] 2606
# exemple avec un script shell
./script.sh &
La commande jobs
Elle permet de lister les process en arrière plan
jobs
[2]+ Running ping google.fr > ping.txt &
# pour tuer un process :
kill -9 2606 << le numéro de process
Attendre que des process en background se terminent avant de lancer un autre process
command 1 &
command 2 &
wait
command3
ex dans un script shell :
#!/bin/bash
sleep 5 &
sleep 10 &
wait
echo "Bonjour"
Comparaison de la conversion de fichier image avec la commande convert (d’ImageMagick)
Nous allons convertir des images jpg en image png avec la commande convert, d’abord sans parallel puis avec parallel.
Pour télécharger les images, les urls sont dans le fichier à télécharger, ce sont des images du site unsplash.com.
Avec une commande en une ligne on va télécharger toutes ces images, je vous conseille de créer un répertoire image
cat images.txt | xargs wget
grâce au piping et avec xargs, et wget.
Mais les images téléchargée doivent être renommées pour que ce soit plus pratique, voici le script à lancer (il n'est pas parfait mais fait le job)
#!/bin/bash
IMAGES=$(ls)
I=0
for IMAGE in $IMAGES
do
MIME=$(file -b --mime-type $IMAGE)
EXT=$(echo "$MIME" | cut -d'/' -f2)
if
mv $IMAGE "image"_${I}.$EXT
I=$((I+1))
done
Ce script va nommer les images avec l’extension trouvée à partir du MIME-type, et numéroter les images. A présent on est prêt pour la conversion. On va utiliser la commande time pour avoir la durée d’exécution
#script bash avec parallel
#!/bin/bash
parallel convert {} {.}.png ::: *.jpeg
# avec parallel
real 1m44,358s
user 6m13,489s
sys 0m21,999s
#script sans parallel
#!/bin/bash
IMAGES=$(ls *.jpeg)
for IMAGE in $IMAGES
do
FILENAME="${IMAGE%.*}"
convert $IMAGE ${FILENAME}.png
done
real 2m26,348s
user 2m39,110s
sys 0m2,865s
Lorsque vous quittez un shell, tous les process lancées sont arrêtés. Ce qui est ok si vous arrêtez de travaillez, mais imaginez que vous avez un process qui prend beaucoup de temps, par exemple la conversion d’une vidéo, vous avez intérêt à garder le shell ouvert.
Cependant il existe une commande Linux qui permet de lancer un process dans un shell et de le fermer sans interrompre son exécution : c’est nohup. Il n’est pas forcément installé par défaut dans ce cas voici ce qu’il faut faire pour l’installer sous Ubuntu/Debian :
sudo apt update
sudo apt install coreutils
Comment utiliser nohup
# par exemple un script de conversion en python
python convertisseur.py
#pour lancer avec la possibilité de fermer le shell sans arrêter le programme
nohup python convertisseur.py
# pour un script shell
nohup ./monscript.sh
# pour une commande
nohup curl -O https://monsite.com
Pour ça s’appelle nohup?
En Linux, il existe un appel système SIGHUP qui va arrêter un process. Nohup va simplement empêcher cet appel d’affecter un process lancé avec lui, d’où le nom No HUP.
Mise en application de nohup
Nous allons lancer un process qui va prendre du temps, mais tout simple, et que nous allons passer avec nohup dans un second temps
ping google.fr
PING google.com (142.250.201.46) 56(84) bytes of data.
64 bytes from mrs08s20-in-f14.1e100.net (142.250.201.46): icmp_seq=1 ttl=115 time=24.5 ms
64 bytes from mrs08s20-in-f14.1e100.net (142.250.201.46): icmp_seq=2 ttl=115 time=13.8 ms
64 bytes from mrs08s20-in-f14.1e100.net (142.250.201.46): icmp_seq=3 ttl=115 time=8.44 ms
64 bytes from mrs08s20-in-f14.1e100.net (142.250.201.46): icmp_seq=4 ttl=115 time=12.3 ms
64 bytes from mrs08s20-in-f14.1e100.net (142.250.201.46): icmp_seq=5 ttl=115 time=8.01 ms
64 bytes from mrs08s20-in-f14.1e100.net (142.250.201.46): icmp_seq=6 ttl=115 time=11.8 ms
64 bytes from mrs08s20-in-f14.1e100.net (142.250.201.46): icmp_seq=7 ttl=115 time=8.41 ms
64 bytes from mrs08s20-in-f14.1e100.net (142.250.201.46): icmp_seq=8 ttl=115 time=9.02 ms
Le ping ne va pas s’arrêter, il prend la main sur le shell. Lançon maintenant avec nohup
nohup ping google.com
nohup: ignoring input and appending output to 'nohup.out'
A ce stade nohup ne vous rend pas la main, mais ouvrez une seconde fenêtre pour afficher le contenu du fichier nohup.out comme indiqué dans le message, vous verrez le même contenu que précédemment. Si vous faites CTRL + C pour arrêter, le process s’arrête. Jusqu’ici nohup n’a pas montré sa valeur ajoutée.
Vous pouvez aussi rediriger vers un fichier spécifique au lieu du fichier par défaut nohup.out.
nohup ping google.com > journal.txt
Un mot à propos de stdout et stderr et stdin
stdout est ce qu’on appelle la sortie standard, en réalité la sortie standard c’est ce que vous voyez à l’écran. Mais il y a stderr, qui est la sortie d’erreur, donc où s’affiche les erreurs? à l’écrant également ! mais on fait le distingo entre la sortie standard et la sortie d’erreur.
On dira que stderr est un flux de sortie, ce qui est très générique comme appellation. ET ne pas oublier qu’il y a stdin ! qui est le flux d’entrée. Il faut savoir que chaque flux possède un numéro de descripteur, stdin a 0, stdout a 1 et stderr a 2. Connaitres ces numéro va nous permettre de mieux comprendre la commande suivante. Donc stdout et stderr affichent tous les deux à l’écran !
Mais on peut faire autrement, ainsi on peut rediriger les erreurs vers un fichier error.log par exemple.
ls /etc /non_existent_dir
ls: cannot access '/non_existent_dir': No such file or directory << erreur
/etc: << sortie standard pas d'erreur
adduser.conf deluser.conf ldap mtab rc2.d sudoers.d
alternatives depmod.d ld.so.cache nanorc rc3.d sudo_logsrvd.conf
apache2 dhcp ld.so.conf netconfig rc4.d sysctl.conf
apparmor dpkg ld.so.conf.d netplan rc5.d sysctl.d
apparmor.d e2scrub.conf legal networkd-dispatcher rc6.d systemd
Ici on essait de faire ls sur un répertoire inexistant, donc ça va provoquer une erreur
ls /etc /non_existent_dir 2> errors.txt
dans l’exemple ci-dessus, on va ridiriger les erreurs vers un fichier errors.txt, et ce qui ne déclenche pas d’erreur sort à l’écran.
ls /etc /non_existent_dir > output.txt 2> errors.txt
L’exemple ci-dessus va rediriger les données vers output.txt, et s’il y a des erreurs, va rediriger vers errors.txt
Une syntaxe plus cryptique:
ls /etc /non_existent_dir > fusion.txt 2>&1
L’exemple ci-dessus va rediriger le flux d’erreur (numéro 2) vers le flux 1, donc les erreurs iront au fichier fusion.txt
Encore un mot sur comment lire stdin
tee input.txt // ce qui est tapé est affiché à l'écran et enregistré dans input.txt
faire CTRL+D pour arrêter
on peut utiliser aussi cat
$ cat > input.txt // ce qui est tapé n'est pas affiché à l'écran, donc pas de doublon à l'écran CTRL+D pour arrêter
Utiliser la commande script pour tout enregistrer dans un fichier
script input.txt // taper exit ou CTRL+D pour sortir
Démarrer un process en background avec nohup (arrière plan)
$ nohup ping google.com &
[1] 8519 // numéro aléatoire
nohup: ignoring input and appending output to 'nohup.out'
Pour afficher le processus faire :
$ pgrep -a ping
8519 ping google.com
pour tuer le process :
kill -9 8519
Cette fois-ci nohup vous rend la main. ON va refaire avec un fichier de sortie
Le MIME-Type est une données qui décrit la nature d’un fichier. Par exemple, un fichier texte aura un MIME Type différent d’un fichier Excel. Une image Jpeg aura un MIME-Type différent d’une image PNG.
Rôles du MIME-Type
Le MIME-Type a plusieurs rôles importants, tout d’abord il permet de connaitre la nature d’un fichier en bypassant l’extension du fichier qui peut être trompeur, ou d’un fichier qui a perdu son extension suite à un renommage.
Ensuite dans les communications entre programmes, il permet au receveurs d’une fichier de connaitre la nature de ce qui est reçu : entre un serveur web et un navigateur web. Dans les requêtes AJAX, c’est application/json qui est envoyé de part et d’autres.
Comment connaitre le MUIME-Type d’un fichier?
Nous allons nous intéresser au shell, avec la commande file (version >= 5)
La commande file nous permet de connaitre le MIME-Type d'un fichier
$ file --mime-type image.png
image.png: image/png
$ file -b --mime-type image.png
image/png
$ file -i image.png
image.png image./png; charset=binary
Connaitre le MIME-type dans deivers langages
#PHP
echo mime_content_type('image.png');
#NodeJS
const mime = require('mime');
const file_path = 'files\file.txt'
const mime_type = mime.getType(file_path)
#Javascript côté client
<input type="file" id="your-files" multiple>
<script>
let control = document.getElementById("your-files");
control.addEventListener("change", function(event) {
// When the control has changed, there are new files
let files = control.files,
for (var i = 0; i < files.length; i++) {
console.log("Filename: " + files[i].name);
console.log("Type: " + files[i].type);
console.log("Size: " + files[i].size + " bytes");
}
}, false);
</script>
Nginx est un serveur web de contenu statique. Mais il a d’autres fonctionnalités qui font son attrait, et parmi les plus souvent cités, le reverse proxy.
Habituellement que vous sollicitez une page web, votre navigateur va directement tutoyer le serveur Apache par exemple ou NodeJs. Par contre si vous mettez un reverse proxy, c’est le reverse proxy qui va être sollicité en premier, et passer la requête ensuite au serveur Apache. Il agit comme un intermédiaire.
Source Wikipedia
Pourquoi s’embête-t-on à mettre un intermédiaire entre le serveur et le navigateur? L’utilité la plus évidente est que c’est une solution pour exposer votre serveur NodeJS qui fonctionne sur le port 3000, ou votre serveur Python, tout en montrant au monde extérieur un port 80, qui est celui par défaut du Web.
Installation de Nginx
Pour faire ce tuto je vais utiliser digitalOcean, une Paas, très pratique et peu cher, à condition de détruire votre instance après usage, bref, pour apprendre le cloud et Docker par exemple c’est idéal.
Je vais créer un droplet Ubuntu avec autenthification par clé SSH, pour éviter d’avoir à me connecter à chaque fois que je vais en mode console.
sudo apt update
sudo apt install nginx
nginx -v
cette commande sert en fait à tester si nginx est bien installé (en plus d'avoir sa version)
Maintenant on va pouvoir démarrer Nginx.
sudo systemctl start nginx
Pour que Nginx puisse démmarer automatiquement au reboot, vous devriez le redémarrer avec la command esuivante :
sudo systemctl enable nginx
# pour checker l'état de Nginx
sudo systemctl status nginx
#il doit mentionner active
Casser le lien avec le fichier de configuration d’origine.
Comme nous allons utiliser Nginx en mode reverse proxy, nous allons casser le lien avec l’encien fichier de configuration et en établir un nouveau.
sudo unlink /etc/nginx/sites-enabled/default
# création d'un nouveau fichier de configuration
sudo vim /etc/nginx/sites-available/reverse-proxy
avec le contenu cidessous dans ce fichier:
server {
listen 80;
server_name localhost;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
La configuration consiste en un bloc server {…} qui gère toutes les requêtes correspondant aux conditions spécifiées. Il contient les éléments suivants :
listen 80 : Le serveur écoute les requêtes HTTP entrantes sur le port 80. server_name localhost : Le nom de domaine pour lequel ce bloc serveur est responsable. L’exemple utilise localhost pour illustrer le fonctionnement du reverse proxy . location / {…} : La configuration pour le chemin d’URI spécifié. Ici, il correspond à toutes les requêtes (/). proxy_pass http://127.0.0.1:8000 : Le serveur backend vers lequel NGINX redirige les requêtes. L’exemple utilise l’adresse locale sur le port 8000 (127.0.0.1:8000) comme backend de substitution. Dans un cas réel, utilisez l’URL du serveur backend approprié. proxy_set_header : La directive permettant de définir des en-têtes HTTP pour la requête proxy. Ces en-têtes sont transmis au serveur backend et fournissent des informations supplémentaires sur la requête du client. L’exemple inclut l’en-tête Host, l’adresse IP du client et le schéma (HTTP ou HTTPS). Remplacez le numéro de port, le nom du serveur et l’adresse du serveur backend par les données réelles. Cet exemple redirige toutes les requêtes faites à localhost vers l’adresse http://127.0.0.1:8000.
Si vous faites des applications Javascript modernes, et requêtez avec la fonction fetch(), vous êtes sans doute confronté à des problèmes de CORS (Cross Origin Request Forgery), en clair vous faites une requête AJAX depuis une nom de domaine différent du nom de domaine du webservice.
Le CORS est une feature et pas un bug : Par exemple, avec le site OpenWeatherMap.org, vous pouvez requêter des données météo depuis votre ordinateur local, qui a un nom de host forcément différent de OpenWeatherMap.org. On fait du CORS. Mais parfois c’est un peu délicat, notamment beaucoup n’ont pas les connaissances théoriques sous jacentes. ça peut devenir compliqué, surtout avec le requêtes PREFLIGHT qui consistent à envoyer un pré-requête pour « tester » la température de l’eau.
Mais le JSONP peut nous être utile, on va voir comment.
Le principe du JSONP
Le JSONP n’est pas une forme de JSON, c’est du JSON, mais utilisé de façon astucieuse en conjonction entre le serveur et le navigateur.
La balise script du front inclut le fichier du back jsonp.php, Ce fichier s’il est exécuté, revoit un réponse texte myFunc({ « name »: »John », « age »:30, « city »: »New York » })
myFunc est définie plus haut dans le script du front, donc en fait elle va être exécutée au moment du chargement de la page.
Un exemple plus intéressant dynamique de JSONP
Ok on a vu que ça marchait, mais c’est un peu statique tout ça, par exemple le snippet d’inclusion du est statique, on va le rendre dynamique, après tout lorsque qu’on fait une requête AJAX c’est souvent sur demande de l’utilisateur non?
Il faut cliquer sur le bouton pour inclure à la demande le fichier jsonp.php.
Et le nouveau fichier du backend, qui renvoit un content-type application/json, comme une vraie réponse AJAX du serveur. De plus on passe au fichier du backend un paramètre GET, pour simuler une demande d’une information particulière (par exemple les informations d’un utilisateur.). On simule succinctement une requêtes à la base de données en fait.
Ok ça marche aussi ! La réponse du serveur est à la demande, comme une requête de type GET.
Quid du cross domaine?
Je vous avait promis du cross domaine au début voyons voir comment ! C’est tout simple on va dans le fichier front modifier le endpoint de fichier jsonp.php, et mettre ce dernier sur un serveur sur Internet. Mon front sera toujours en local, donc on est bien en situation de cross domaine.
function clickButton() {
let s = document.createElement("script");
s.src = "http://glottr.com/jsonp.php?x=Yvon";
document.body.appendChild(s);
}
C’est là que c’est intéressant. Vous voyez comment on peut inclure le fichier jsonp.php ? C’est dans la balise <script> et dans ce cas, on peut inclure un script de n’importe quel domaine.
Bougeons le fichier et regardons, le résultat est le même !
On peut faire plus sophistiqué avec JSONP
Jusqu’ici la fonction myFunc était codée dans le serveur, comment ferait on si on ne sait pas à l’avance la fonction à invoquer côté client? Vous pouvez passer le nom de la fonction callback en paramètre GET
si vous êtes depuis un moment dans le monde de Javascript NodeJs et autres framework front end, vous avez dû déjà croiser au moins 3 de ces 5 programmes.
Mais à quoi servent ils? Nous allons voir un par un ces gestionnaires de paquets.
NPM Node Package Manager
Il est né en même temps que Node JS. Très largement utilisé en général. Il permet d’installer, d’éxécuter un process (build par exemple)
NVM Node Version Manager
Il vous permet d’installer plusieurs version de NodeJS. Il vous permet de chcker la version de NodeJS
NPX Node Package Execute
Il est inclus avec NPM à partir de la version 5.2 de NPM. Le X comme execute permet à NPX d’exécuter des paquets. Oui mais NPM sait aussi exécuter un package, mais à condition qu’il soit installé en global. Si un paquet est installé en local, il est local au projet, càd dans le répertoire node_modules du projet.
C’est là que NPX prend la relève, ce dernier est capable d’exécuter un paquet. Il va d’abord chercher dans la variable PATH puis dans les fichiers binaire. S’il ne trouve pas, il est capable d’aller chercher sur Internet.
YARN Yet Another Resource Negociator (Facebook)
Yarn installe les paquets en parallèle, et donc il est plus rapide.
PNPM Performant NPM
PNMP utilise un endroit centralisé pour installer les paquets. Avec NPM vous pouvez installer un paquet en global ou en local.
Le shell est le moyen le plus puissant d’interaction avec le système d’exploitation. Il existe types de shell : le primitif sh (bourne shell l’original), puis csh (C shell), puis bash (Bourne again shell), ksh (Korn shell), tsh T shell, tcsh, et zsh (j’en ai peut être oublié)
Nous allons installer le petit dernier sur Linux, zsh est intéressant car il a plus de fonctionnalité, dont la capacité de prévisualiser l’autocomplétion, mais aussi facilement personnalisable avec les thèmes.
installation de zsh
D’abord mettons à jour les dépôts
sudo apt update
sudo apt install zsh -y # répond yes à toutes les questions pour aller plus vite
Ensuite vérifiez l'installation
zsh --version
Configuration de zsh
Mais avant d’aller configurer zsh, il faut le démarrer. EN passant voyons comment connaitre le shell courant
Quel est mon shell courant sous Linux?
il y a plusieurs façon de le faire
Méthode 1 : affiche le shell courant
echo $0
Méthode 2
echo $SHELL
cette dernière méthode n'affiche pas le vrai shell courant, car si vous changez de shell, l'affichage ne changera pas, car c'est une variable d'environnement.
Pour changer de shell, tapez zsh dans le terminal, et vérifiez que c’est bien le shell courant. Pour savoir où se trouve le chemin vers le programme shell
which zsh
La première fois que vous démarrez le zsh, vous devez le configurer à la différence des autres shell
Appuyez sur q pour quitter la configuration et recommencer la prochaine fois que vous entrerez dans Zsh.
Appuyez sur 0 pour créer un fichier de configuration .zshrc vide et tout configurer à partir de zéro.
Appuyez sur 1 pour accéder au menu principal et configurer chaque paramètre individuellement.
Appuyez sur 2 pour remplir le fichier de configuration .zshrc avec les paramètres par défaut, que vous pourrez ensuite modifier manuellement dans le fichier .zshrc.
Si plus tard vous voulez reconfigurer le zsh tapez la commande
zsh-newuser-install
La commande chsh
Cette commande permet de setter le shell par défaut pour un utilisateur
chsh zsh /chemin/vers/shell/. <username>
souvenez vous que which zsh donne le chemin vers le shell
chsh zsh `which zsh` <username>
En cas d’erreur PAM: Authentication failure
Il s’agit d’une configuration inadéquate de votre fichier /etc/pam.d/chsh, ouvrez le et /
Remplacéez
auth required pam_shells.so
par
auth sufficient pam_shells.so
Installer Oh My Zsh pour avoir plus de fonctionnalité
sh -c "$(wget https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh -O -)"
sur MacOS on utilise Curl car wget n'est pas installé par défaut
sh -c "$(curl -fsSL https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
Copie d’écran MacOS.
OhMy Zsh est une collection de 150 thèmes voici le github. Pour changer de thème il faut éditer le fichier .zshrc
et localiser la ligne ZSH_THEME
rempalcer la ligne
ZSH_THEME="robbyrussel" par
ZSH_THEME="jonathan" par exemple
Installer le plugin autosuggestion
Une des choses les plus sympathique de zsh est l’autosuggestion, avant que vous ayez fini de taper une commande, une liste de choix se propose à vous. installons le plugin
git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions
ouvrez .zshrc et localisez la ligne
plugins=(git) et changez la en
plugins=(git zsh-autosuggestions)
redémarrez le terminal
Quand vous tapez un début de commande, utilisez la flèche de droite (et non la touche Tab) pour choisir al suggestion.
Installer le surlignement de syntaxe (syntax highlighting)
Ceci vous permet une mielleure lisibilité et esthétisme
tmux est un programme qui permet de splitter son écran shell en plusieurs partie appelé en anglais « pane ». De plus il permet d’avoir plusieurs « window » un peu comme les bureau sous Windows ou MacOS. Mais nous allon spour simplier ne considérer qu’une seule window.
TL;DR;
Redimensionner un panneau : CTRB + B puis CTRL + flèche
pour lancer tmux rien de plus simple :
$ tmux
l'affichage va un peu changer, car vous avez initié une session tmux, maintenant nous allons diviser l'écran en deux "pane" verticaux. Pour ce faire faites d'abord la combinaison CTRL + B puis % (pourcentage), vous obtenez deux panes verticaux.
Commandes de base
Voyons comment on peut changer de pane. Faites la commande CTRL+B puis flèche gauche ou droite.
Pour fermer un pane, CTRL+B puis « x ». Pour fermer un window CTRL+B puis « & », une confirmation est demandée à chaque fois.
Pour afficher l’aide de tmux : tmux info
Pour afficher les jnuméro des pane : CTRL + B puis q, identifier le numéro permet de basculer vers un pane sans passer par tous les panes, pendant que les numéros restent affichés, appuyer sur un nombre pour y basculer directement.
Pour splitter un pane horizontalement : CTRL + B puis » (guillemets)
Manipulation importante : autocomplétion
Par défaut l’autocomplétion n’est pas active, il faut toucher au fichier de configuration .tmux.conf, qui n’existe pas par défaut, il faudra le créer. Cependant, vous pouvez afficher la configuration de tmux avec la commande suivante:
tmux show -g
On va pouvoir piper le contenu vers un fichier .tmux.conf qu'on va mettre dans le répertoire home de l'utilisateur
tmux show -g | cat > ~/.tmux.conf
il ne vous reste plus qu'à éditer ce fichier en ajoutant la ligne suivante:
unbind -n Tab
sauvez le fichier. Pour vous assurer que c'est actif redémarrez tmux.
Raccourcis clavier de tmux:
Ctrl-b + d – se détacher de la session en cours.
Ctrl-b + % – divise une fenêtre en deux volets, l’un supérieur et l’autre inférieur.
Ctrl-b + ” – divise une fenêtre en deux volets verticalement.
Ctrl-b + flèche – permet de changer de volet dans une direction donnée.
Ctrl-b + x – fermer le volet en cours.
Ctrl-b + c – créer une nouvelle fenêtre.
Ctrl-b + n – passer à la fenêtre suivante en fonction du numéro.
Ctrl-b + p – retour à la fenêtre précédente.
Ctrl-b + numéro d’identification – permet d’accéder à une fenêtre spécifique à l’aide du numéro d’identification.
Ctrl-b + : – ouvre le mode ligne de commande.
Ctrl-b + ? – imprimer tous les raccourcis.
Ctrl-b + w – liste toutes les fenêtres de la session en cours.
Commandes tmux:
tmux info - liste les commandes
Navigation entre les panneau
Sans modification du fichier de configuration
CTRL + B puis o
CTRL + B + o va déplacer un pane
Dans le fichier .tmux.conf ajoutez les lignes:
#cycle dans les pane
bind -n S-right select-pane -t :.+
bind -n S-left select-pane -t :.-
rechargez la configuration : CTRL +B, puis :source-file ~/.tmux.conf
vous pouvez SHIFT + flèche gauche droite pour cycler dans les pane.
Tmux et les sessions
Lorsque vous faites tmux tout seul vous démarrez une session anonyme. Mais il peut être intéressant de démarrer une session nommée, si vous en démarrez plusieurs
Tmux créer une session nommée
tmux new -s masession
et dans la session même si vous faites tmux ls ou tmux list-session
Ne tentez pas de démarrer une session tmux dans une session tmux, vous aurez le message de découragement suivant : sessions should be nested with care, unset $TMUX to force
Une meilleure pratique est de se détacher d’une session tmux pour revenir à la session principale et d’en démarrer une nouvelle
Se détacher d’une session tmux
CTRL + B puis D
0: 1 windows (created Sat Mar 22 11:00:35 2025) (attached)
session1: 1 windows (created Sat Mar 22 11:01:31 2025) << session détachée
Pour se rattacher à une session
tmux a // se rattacher à la dernière session quittée
tmux a -t masession // se rattacher à une session nommée
Si vous quittez votre shell et que vous vous rattachez à l aprochaine session shell c’est possible.
Sortir d’une session Tmux d’un seul coup
tmux kill-server
Si vous avez plusieurs fenêtre ou panneau, pour sortir sans avoir à fermer tous les panneau
Les fenêtres dans tmux
Pour vous dire que tmux a beaucoup de capacité, jusqu’ici vous avez fait connaissance avec des panneaux ou pane en anglais, mais ces panneau sont en réalité dans une fenêtre !
# se détacher de la fenêtre et démarrage d'une nouvelle session avec une session nommée et une fenêtre nommée
$ tmux new -s session2 -n maFenetre
Ensuite faire la commande tmux ls
Pour afficher les fenêtre : CTRL + D puis W
Pour changer de fenêtre : CTRL +D puis N
Résultat de la commande CTRL + D puis W
Dans cette vue vous pouvez changer de fenêtre avec le raccourcis CTRL + D puis N
Sinon vous voyez qu’il y a un numéro à côté d’une fenêtre, on peut naviguer par numéro : CTR L + B puis numéro de fenêtre.
Ngrok est (était) un service pratique pour partager votre site web en local à votre client, mais aujourd’hui il nécessite un compte. Il y a des alternatives comme localtunnel, ou serveo. Mais je vais vous ontrer qu’il est possible de faire sans. Pour ce faire il faut que vous disposiez d’un VPS.
Pour l’exemple je me base sur un Droplet DigitalOcean avec authentification par mot de passe.
Vérifiez juste que le firewall (iptable ou ufw) ne bloque par l’accès au port 22 et que le /etc/ssh/sshd_config contienne les lignes suivantes sur le VPS:
GatewayPorts yes
PermitRootLogin yes
Ensuite et c’est là où il faut comprendre qu’il faut faire la commande en local et non sur le VPS !! :
ssh -R 80:localhost:3000 root@159.223.3.3
La commande ci-dessus forwarde le port 80 du serveur VPS (identifié par son adresse IP) vers le localhost:3000.
Il va vous être demandé une authentification (mot de passe), une fois ceci fait, allez dans le navigateur et entrez l’adresse :
http://159.223.3.3
et vous verrez le contenu de votre localhost:3000 !
netcat est un utilitaire réseau servant à écouter les ports. Par exemple l’exemple suivant va écouter sur le port 12345
# ouvrez un premier shell et faites la commande suivante:
nc -l -p 12345
# depuis le même hôte ouvrez un second shell et faites la commande suivante :
echo "Hello" | nc 127.0.0.1 12345
dans le premier shell vous allez voir le message "Hello" sortir
L’exercice serait plus intéressant si on envoyait un message depuis un ordinateur extérieur ayant une autre adresse IP:
# dans le premier shell de l'hôte avec l'adresse IP 145.231.65.88 (adresse donnée au hasard)
nc -l -p 12345
# dans le second ordinateur
echo "Hello" | nc 145.231.65.88 12345
# il ne se passe rien, parce que par défaut netcat n'écoute que sur l'interface 127.0.0.1, pour lui demander d'écouter sur toutes les interfaces :
nc -l -p 12345 -s 0.0.0.0 -k -v
cette fois ci ça marche.
Listening on 0.0.0.0 12345
Connection received on 5.48.206.214 63696
Hello
Rediriger ver un named pipe.
Ce que netcat va recevoir on le redirige vers un named pipe.Il faut d’abord créer un pipi appelé mypipe
# dans un premier shell
mkfifo mypipe
nc -l -p 12345 -s 0.0.0.0 -k -v > mypipe
# dans un second shell
cat < mypipe
# depuis un ordinateur distant
echo '9' | nc 178.62.221.128 12345
# vous constaterez qu'il y a un petit délai d'une seconde pour voir afficher
# mais le plus gênant c'est du côté de l'ordinateur distant, on ne nous rend pas la main, ceci est dû au fait que netcat garde la connexion ouvert après l'envoi. Nous allons lui demander de fermer la connexion après envoi avec le paramètre -q à 1
$ echo '7' | nc -q 1 178.62.221.128 12345
la main nous est rendu après envoi.
Rediriger vers un fichier texte
au lieu de rediriger vers le named pipe on redirige vers un fichier
$ nc -l -p 12345 -s 0.0.0.0 -k -v >> log.txt
Oubliez le web, revenez 30 ans en arrière, comment faisaient les process (programmes) pour communiquer?
Les named pipe sont une technologie qui le permettent, mais il y a aussi les sockets. Mais dans cet article on va parler uniquement des named pipes.
Mais d’abord c’est quoi les pipes?
Dans Linux un pipe est représenté par le caractère | (une barre verticale)
commande 1 | commande 2
Dans l’exemple ci-dessus, la sortie de la commande 1 est passée en entrée de la commande 2.
Second exemple:
ps aux
la commande ci-dessus nous permet de lister tous les process en cours.
.......
apple 79180 0,0 0,2 67548168 32508 ?? S 9:40 0:00.29 /Applications/Go
apple 75919 0,0 0,0 34151264 1908 s005 S 9:25 0:00.08 -bash
root 75918 0,0 0,0 34151500 4856 s005 Ss 9:25 0:00.03 login -pf apple
apple 75772 0,0 0,0 33619312 2640 s004 Ss+ 9:25 0:00.03 /usr/local/bin/b
.......
Il y en a des centaines. On va filtrer la ou les lignes qui contiennent le mot ‘bash’, (vous pouvez choisir le mot que vous voulez). Pour ce faire on va utiliser grep qui permet de chercher dans un fichier un mot ou une expression.
ps aux | grep bash
apple 75919 0,0 0,0 34151264 1908 s005 S 9:25 0:00.08 -bash
apple 75772 0,0 0,0 33619312 2640 s004 Ss+ 9:25 0:00.03 /usr/local/bin/bash --init-file /Applications/Visual Studio Code.app/Contents/Resources/app/out/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh
apple 88204 0,0 0,0 34122828 836 s007 S+ 10:20 0:00.00 grep bash
apple 83845 0,0 0,0 34151264 1924 s007 S 10:00 0:00.04 -bash
apple 81464 0,0 0,0 34151264 1876 s006 S 9:50 0:00.03 -bash
la réunion de deux commandes avec pipe nous permet d’avoir un affichage plus restreint.
C’est quoi un named pipe?
un named pipe est un fichier qui permet de faire transiter des messages, dans la logique du premier entré premier sorti ( First In First Out en anglais, sous l’acronyme FIFO). Le fait que ce soit un fichier (un peu spécial) fait qu’il est persistent, jusqu’à ce qu’il soit effacé. Un named pipe permet de faire communiquer des process qui n’ont aucun lien entre eux ! Vous pouvez imaginer comme un téléphone pour les programmes.
Création d’un named pipe
la création se fait avec la commande mkfifo
$ mkfifo mypipe
ls -l
total 0
prw-r--r-- 1 apple staff 0 15 fév 21:42 mypipe
Regardez la sortie de la commande ls -l, la première lettre est « p » comme pipe.
Ecriture et lecture d’un named pipe
Dans ce terminal écrivez quelque chose et passez le au pipe :
echo 'Hello le named pipe!' > mypipe
après avoir écrit cette commande, le shell ne vous rend pas la main
ouvrez un second terminal
et tapiez la commande
$ cat < mypipe
Hello le named pipe!
Un exemple plus consistant
Ouvrez un premier terminal et mettez ce code shell :
$ while true;do
> echo "message at $(date)" > mypipe
> sleep 2
> done
Ce script va toutes les 2 secondes afficher le message avec la date et l’envoyer à mypipe. (Pour taper un script multiligne, utilisez la combinaison SHIFT + ENTREE pour sauter une ligne.)
Ouvrez un second terminal et tapez la commande pour lire la sortie de mypipe:
cat < mypipe
message at Sam 15 fév 2025 22:32:53 CET
$
Aussitôt le message lu le shell vous rend la main Essayez d’enchainer rapidement deux cat < mypipe, si rien n’est dispo à la sortie, le shell ne vous rend pas la main. Notez que si vous attendez un peu trop longtemps,les messages se perdent donc il faut lire de façon continue, ce que nous allons faire avec tail.
tail -f < mypipe
Faire une boucle pour écouter continuellement des message
#Le code ci-dessous va écouter en permanence des messages venant du pipe
while true; do cat < mypipe; done
# ouvrez un second terminal dans le même hôte et faites
$ echo "un message" > mypipe
#le code ci dessus va envoyer un message au pipe et rendre la main.
Ecrire sur un named pipe vers un hôte distant (A venir)
Avec Linux, il est très facile via SSH de traverser la frontière du cyberespace !Allez sur un serveur Linux er créez un named pipe.
Vous avez sans doute entendu parler de iptables, ce firewall très connu sous Linux, mais peut être u peu difficile à configurer car très technique et verbeux. Il existe un autre équivalent beaucoup plus friendly : UFW. UFW est un e interface d’accès à iptables, pour faciliter l’usage de ce dernier.
Un pare feu a pour rôle de bloquer le traffic réseau sortant ou entrant, sur le protocole que vous voulez (par exemple SSH, HTTP etc). Vous pouvez blocker des ports, des adresses IP et des intervalle d’adresses IP.
Installation de UFW et activation
Si ce n’est déjà installé vous pouvez faire la commande suivante:
$ sudo apt update
$ sudo apt upgrade
# vérification préalable de l'existence de UFW
$ which ufw
# le cas échéant
$ sudo apt-get install ufw
# après installation on véirifie l'état d'UFW
$ sudo ufw status verbose
-----------------------------
Output
Status: inactive
# on active UFW
$ sudo ufw enable
#pour désactiver
$ sudo ufw disable
La commande status est importante pour avoir l’état des lieux, je vous recommande de souvent la faire pour diagnostiquer des erreur.
On eptu utiliser une autre command pour avoir le status
systemctl status ufw
● ufw.service - Uncomplicated firewall
Loaded: loaded (/lib/systemd/system/ufw.service; enabled; vendor preset: enabled)
Active: active (exited) since Sun 2025-02-16 09:12:11 CET; 30min ago
Docs: man:ufw(8)
Main PID: 129 (code=exited, status=0/SUCCESS)
Feb 16 09:12:11 PC-YVON systemd[1]: Starting Uncomplicated firewall...
Feb 16 09:12:11 PC-YVON systemd[1]: Finished Uncomplicated firewall.
Autoriser un port sur un protocol : port 80 sur TCP
# va autoriser les connexion HTTP (web)
$ sudo ufw allow 80/tcp
Rule added
Rule added (v6)
Status: active
Logging: on (low)
Default: allow (incoming), allow (outgoing), disabled (routed)
New profiles: skip
To Action From
-- ------ ----
80/tcp ALLOW IN Anywhere
80/tcp (v6) ALLOW IN Anywhere (v6)
Nettoyer toutes les règles:
ufw reset
# après redémarrer
ufw enable
Liste des commandes de UFW:
Usage: ufw COMMAND
Commands:
enable enables the firewall
disable disables the firewall
default ARG set default policy
logging LEVEL set logging to LEVEL
allow ARGS add allow rule
deny ARGS add deny rule
reject ARGS add reject rule
limit ARGS add limit rule
delete RULE|NUM delete RULE
insert NUM RULE insert RULE at NUM
prepend RULE prepend RULE
route RULE add route RULE
route delete RULE|NUM delete route RULE
route insert NUM RULE insert route RULE at NUM
reload reload firewall
reset reset firewall
status show firewall status
status numbered show firewall status as numbered list of RULES
status verbose show verbose firewall status
show ARG show firewall report
version display version information
Application profile commands:
app list list application profiles
app info PROFILE show information on PROFILE
app update PROFILE update PROFILE
app default ARG set default application policy
// Principales commandes de ufw
===============================
sudo ufw enable
sudo ufw disable
sudo ufw status
sudo ufw status numbered
sudo ufw allow 22
sudo ufw allow http
sudo ufw allow 8080/tcp
sudo ufw deny from 192.168.1.100 "192,168,1,255 adresse de broadcast
decouverte de réseau
imprimante qui annonce sa disponibilité"
sudo ufw deny 23
sudo ufw status numbered
sudo ufw delete NUMERO
sudo ufw reset
sudo ufw allow from 192.168.1.0/24 "notation CIDR sert à représenter un bloc d'adresses IP, IP est codé sur 32 bits, ici 24 premiers bits sont fixes
Quelle plage d’adresses couvre ce bloc ? 192.168.1.1 à 192.168.1.254
/24 correspond à 255.255.255.0 (masque de sous réseau)"
sudo ufw deny from 10.0.0.0/8
sudo ufw limit ssh
iptables est un firewall de Linux. La configuration est vaste car en matière de réseau il y a différents types de protocole. Par exemple si vous pingez une adresse IP c’est le protocole ICMP qui est en jeu. nftables est censé remplacer iptables.
Installation de iptables
apt install iptables
// connaitre la version de iptables
iptables --version
Concepts clé d’iptables
Tables, les 4 tables filter, nat, mangle,raw
Chains INPUT OUTPUT, FORWARD
Policies comportement par défaut d’une chaine (ACCEPT or DROP)
Rules règles appliquées au réseau
Lister et voir les règles
Pour voir les règles tapez la commande suivante :
iptables -L
Pour lister les règles avec leur numéro :
iptables -L --line-numbers
La politique par défaut c’est quoi?
Imaginez une clause switch case, le cas par défaut est celui qui n’est couvert par aucune règle.
Comment définir une politique par défaut?
avec le flag -P
iptables -P <chaine> <cible>
iptables -P INPUT DROP
iptables -P OUTPUT ACCEPT
iptables -P FORWARD ACCEPT
Le flag -P ne crée pas de nouvelle règle.
Exemple de politique par défaut
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT
Exemple de règles
Ajouter une règle d’interdiction pour les ping (protocole ICMP)
#Tapez la commande suivante pour ajouter une règle à iptables
iptables -I INPUT -p ICMP --icmp-type 8 -j DROP
Bloquer les demandes SSH
iptables -A INPUT -i eth0 -p tcp --dport 22 -j DROP
Supprimer un règle
//Pour supprimer les règles :
supprime la règle 1
iptables -D INPUT 1
Création de règles
Autoriser SSH
sudo iptables -A INPUT -p tcp --dport 22 -m state --state NEW,ESTABLISHED -j ACCEPT
sudo iptables -A OUTPUT -p tcp --sport 22 -m state --state ESTABLISHED -j ACCEPT
Autoriser HTTP et HTTPS
sudo iptables -A INPUT -p tcp --dport 80 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 443 -j ACCEPT
Autoriser le ping (ICMP)
iptables -A INPUT -p icmp -j ACCEPT
Autoriser le traffic local (loopback)
iptables -A INPUT -i lo -j ACCEPT
Refuser une IP spécifique
iptables -A INPUT -s 192.168.1.100 -j DROP
Limitation brute force
sudo iptables -A INPUT -p tcp --dport 22 -m state --state NEW -m recent --set --name SSH
sudo iptables -A INPUT -p tcp --dport 22 -m state --state NEW -m recent --update --seconds 60 --hitcount 4 --name SSH -j DROP
Exemple de configuration typique d’un serveur
# Vider les règles existantes
iptables -F
iptables -X
# Politique par défaut
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT
# Autoriser loopback
iptables -A INPUT -i lo -j ACCEPT
# Autoriser les connexions établies
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
# Autoriser SSH
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
# Autoriser HTTP / HTTPS
iptables -A INPUT -p tcp --dport 80 -j ACCEPT
iptables -A INPUT -p tcp --dport 443 -j ACCEPT
# Autoriser ping
iptables -A INPUT -p icmp -j ACCEPT
// on sauvegarde les règles
iptables-save > /etc/iptables/rules.v4
C’est quoi le loopback?
C’est l’interface réseau de votre ordinateur, appelé lo (lettre l et lettre o)
Fail2ban, c’est un outil de sécurité pour les serveurs, principalement utilisé sous Linux. Son rôle principal est de protéger le serveur contre les attaques par force brute (par exemple, quand un attaquant essaie de deviner un mot de passe en tentant plein de fois de se connecter).
Voici ce que fait fail2ban concrètement :
Il surveille les fichiers logs des services (comme SSH, FTP, Apache, etc.) pour détecter des tentatives de connexion échouées répétées.
Dès qu’il remarque qu’une même adresse IP tente plusieurs fois sans succès (configurable), il va bloquer cette IP temporairement via le pare-feu (iptables, nftables, etc.).
Le blocage est temporaire (par exemple 10 minutes, 1 heure), mais si l’IP continue à poser problème, fail2ban peut prolonger ce blocage.
Cela limite efficacement les risques d’intrusion par brute force sans que tu aies à intervenir manuellement.
Ce que n’est pas fail2ban
Fail2ban ne va pas vous bloquer tout seul, il a besoin d’un firewall comme iptables pour le faire ou ufw. Fail2ban met juste en place des IP à bloquer.
Installer fail2ban
Si vous êtes sous Debian faites la commande suivante :
sudo apt install fail2ban
#apt est une nouvelle version de la commande apt-get, designé pour être plus ergonomique
Ensuite dupliquez les fichiers fail2ban.conf et jail.conf e, fail2ban.local et jail.local, c’est une habitude à prendre pour en cas de mise à jour de fail2ban, vous perdiez votre configuration.
Configurer fail2ban pour protéger ssh
Nous allons modifier avec vim ou nano le fichier jail.local. Comme nous voulons protéger dans cet article seulement sshd, nous allons trouver la section [sshd] et nous assurer de trouver les lignes suivantes
[sshd]
maxretry=2
findtime=600 #10 minutes soit 600 secondes
bantime=600 # temps d ebanissement
bantime est le temps de banissement, findtime est le temps durant lequel les tentatives infructueuses de login se déroulent, c’est la fenêtre d’observation.
Expérimentation:
Connectez vous à un compte et faites exprès d’échouer 2 sessions de connexion de suite.
Ensuite tentez de vous connecter à nouveau.
Quelques commandes relatives à fail2ban
connaitre l’état des lieux de fail2ban
# connaitre l'état des lieux des bans pour un programme comme sshd
fail2ban-client status sshd
#Redémarrer fail2ban après une modification de fichier d configuration
sudo systemctl restart fail2ban
Le fichier de log de fail2ban se trouve dans /var/log/fail2ban.log
en conjonction de fail2ban, il est intéressant de monitorer les logs de connexion du fichier auth.log. Si ce fichier n’est pas présent c’est qu’il faut installer via la command suivante :
Pour tout ce qui suit, il faut avoir sous Windows ou MacOS démarré Docker Desktop.
Déploiement d’un site pur front end mono container
C’est le cas le plus simple.
Pour rappeler une image Docker est construite en copiant les fichiers d’un projet vers une image qui sera « buildée ». Cette image sera runnée pour devenir un container.
Déploiement d’une application PHP/MySQL multicontainer
Une application PHP/MySQL est fait d’un script PHP et d’une base de données. On ne peut pas mettre dans une seule image, donc il nous faudra deux images, les runner sous forme de deux containers. Il faut que la bse de données soit disponible avant que le script PHP ne soit exécuté.
Le script PHP va faire une requête en base de donnée, donc il faut que les deux containers puissent communiquer entre eux.
Enfin on doit pouvoir visiter l’application PHP depui snotre ordinateur hôte.
FROM php:7.3-apache
#Install git and MySQL extensions for PHP
RUN apt-get update && apt-get install -y git
RUN docker-php-ext-install pdo pdo_mysql mysqli
RUN a2enmod rewrite
COPY src /var/www/html/
EXPOSE 80/tcp
EXPOSE 443/tcp
On va builder une image pour le projet PHP, pour l’image de MySQL on va prendre l’image officielle de MySQL.
Et pour mettre en musique les deux images, on a le fichier docker-compose.yml
version: '3.8'
volumes:
mysql_data:
# Persistent storage for MySQL data
services:
mysql:
image: mysql:8.0
container_name: mysql8
environment:
MYSQL_ROOT_PASSWORD: secret
MYSQL_DATABASE: app_db # Create a default database
MYSQL_USER: app_user # Create a non-root user
MYSQL_PASSWORD: app_password # Password for the non-root user
ports:
- "3306:3306" # Optional: Expose MySQL port to host
volumes:
- mysql_data:/var/lib/mysql # Persist database files
restart: always
php:
container_name: php_app
build:
context: .
dockerfile: Dockerfile
ports:
- "8080:80" # Map port 80 in the container to port 8080 on the host
depends_on:
- mysql # Ensure MySQL starts before PHP
environment:
MYSQL_DB_HOST: mysql # MySQL service hostname
MYSQL_DB_PORT: 3306
MYSQL_DB_USER: app_user
MYSQL_DB_PASS: app_password
MYSQL_DB_NAME: app_db
Ne soyez pas effrayés par la syntaxe, il y a un formalisme qui reste le même pour ce genre de fichier.
Ici on a deux images, l’image de Mysql est configurée avec le mot de passe root, nom de la bdd, du user et le mot de passe du user non root. On mappe le port 3306 de l’hôte (à gauche des deux points) à celui du container (à droite).
Pour le container de l’application PHP, on définit des variables d’environnement qui sont bien sûr un peu identique aux pramétrage de du container MySQL, mais on n’est pas obligé de les avoir et on peut coder en dur dans le script les mots de passe et autre nom de base de donnée. On mappe le port 8080 de l’hôte au port 80 du container. Pour lancer l amachinerie faites :
docker-compose up
docker-compose docn pour arrêter
# et si vous faites des modifications il faut rebuilder l'image
Vous allez peut être rencontrer un soucis pour la connexion à la base de données, MySQL8, qui utiliser un hashage nouveau, il vous faudra rentrer dans le docker de MySQL et taper la commande suivante:
docker exec -it mysql8 mysql -uroot -psecret
//mysql native password use sha1, newer method use sha2 but not supported by PDO, so roll back to native password
//et une fois dans le prompt de MySQL:
//exécutez la requêtes SLQ suivante :
ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY 'secret';
FLUSH PRIVILEGES;
Cette commande a pour but d’utiliser l’ancien algorithme de hashage de mot de passe.
Le script PHP
C’est un simple script qui fait une connexion à al base de donnée et va dumper l’objet PDO
<!DOCTYPE html>
<html lang="en">
<head>
<title>Show databases in MySQL server</title>
<meta http-equiv="content-type" content="text/html; charset=iso-8859-1" />
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
</head>
<body>
<div class="container">
<h1>Show databases in MySQL server</h1>
<?php
$dsn = 'mysql:host=mysql;dbname=app_db';
$user = "root";
$pass = "secret";
// Création de l'objet de connexion qui va nous permettre de faire des requêtes SQL
$pdo = new \PDO($dsn, $user, $pass);
$result = $pdo->exec("SET CHARACTER SET utf8");
var_dump($pdo);
?>
</div>
</body>
</html>