Faire tourner une distribution Debian dans un container docker

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 

Installer Proftpd sous Debian dans WSL et accéder via FileZilla depuis l’hôte Windows

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

netsh interface portproxy add v4tov4 listenport=21 listenaddress=127.0.0.1 connectport=21 connectaddress=172.18.104.160

Protocole FTP

Si vous voulez en savoir plus sur les verbes FTP qui permettent de télécharger des fichiers et autres manipulation, visitez le lien ci-dessous :

https://www.cs.colostate.edu/helpdocs/ftp.html

Construire un serveur de chat en shellscript avec socat !

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)

Mixer les ESmodules et les modules CommonJS dans un projet NodeJS

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

{
  "name": "cloneflicker",
  "private": true,
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview",
    "lint": "eslint -c 'eslint.config.js' src"
  },
....

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 fichiersComportement en type= »commonjs »Comportement en type= »module »
.cjsCommonJS moduleCommonJS module
.jsCommonJS moduleES Module
.mjsES ModuleES 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

Je vous donne ct exemple tiré de cet article :

https://pencilflip.medium.com/using-es-modules-with-commonjs-modules-in-node-js-1015786dab03

// module1.js
console.log("require module1");
import obj from "./module2.cjs";
console.log(`module2 = ${obj.module2}`);
// module2.cjs
console.log("require module2");
exports.module2 = "require module2";

Combiner netcat et les named pipe pour un système d’écoute à travers le réseau

Soit le script suivant (inspiré de cet article sur Stackoverflow)

Utiliser un named pipe combiné à un serveur netcat pour écouter des messages sur localhost

Si vous n’êtes pas familier des named pip, lisez cet article sur les named pipe

#!/bin/sh
request=pipe
rm "$request"
mkfifo "$request"

HandleCommand () {
  echo "received command: $1"
  [ "$1" = "exit" ] && return 1
  return 0
}

while :; do
  while read line; do
    HandleCommand "$line" || break
  done <"$request" | nc -l -p 5000 >"$request"
done

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

#!/bin/sh
request=pipe
rm "$request"
mkfifo "$request"

HandleCommand () {
  echo "received command: $1"
  [ "$1" = "exit" ] && return 1
  return 0
}

while :; do
  while read line; do
    HandleCommand "$line" || break
  done <"$request" | nc -l -p 5000 -s 0.0.0.0 >"$request"
done

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

Redirection de port avec Powershell

Ajout d’une règle de port forwarding

Powershell est asse imbittable comme syntaxe !

netsh interface portproxy add v4tov4 listenport=5000 listenaddress=0.0.0.0 connectport=5000 connectaddress=172.18.104.160

Ensuite nous allons agir sur le firewall pour autoriser les connexions sur le port 5000.

Autoriser le port 5000 en écoute sur Windows

New-NetFirewallRule -DisplayName "Allow Port 5000" -Direction Inbound -Protocol TCP -LocalPort 5000 -Action Allow

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

ss -tlnp | grep 5000

LISTEN 0      128          0.0.0.0:5000       0.0.0.0:*     users:(("nc",pid,fd))

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.

Cet article traite de ce sujet !

Introduction au machine learning avec un modèle de prédiction d’abandon de souscription

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

df.info()

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 7043 entries, 0 to 7042
Data columns (total 21 columns):
 #   Column            Non-Null Count  Dtype  
---  ------            --------------  -----  
 0   customerID        7043 non-null   object 
 1   gender            7043 non-null   object 
 2   SeniorCitizen     7043 non-null   int64  
 3   Partner           7043 non-null   object 
 4   Dependents        7043 non-null   object 
 5   tenure            7043 non-null   int64  
 6   PhoneService      7043 non-null   object 
 7   MultipleLines     7043 non-null   object 
 8   InternetService   7043 non-null   object 
 9   OnlineSecurity    7043 non-null   object 
 10  OnlineBackup      7043 non-null   object 
 11  DeviceProtection  7043 non-null   object 
 12  TechSupport       7043 non-null   object 
 13  StreamingTV       7043 non-null   object 
 14  StreamingMovies   7043 non-null   object 
 15  Contract          7043 non-null   object 
 16  PaperlessBilling  7043 non-null   object 
 17  PaymentMethod     7043 non-null   object 
 18  MonthlyCharges    7043 non-null   float64
 19  TotalCharges      7043 non-null   object 
 20  Churn             7043 non-null   object 
dtypes: float64(1), int64(2), object(18)
memory usage: 1.1+ MB

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

sns.boxplot(x='Churn', y='MonthlyCharges', hue='Churn', data=df, palette='Set2', dodge=False, legend=False)

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.

Préparation des données pour la modélisation

Rappel
df.info()

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 7043 entries, 0 to 7042
Data columns (total 21 columns):
 #   Column            Non-Null Count  Dtype  
---  ------            --------------  -----  
 0   customerID        7043 non-null   object 
 1   gender            7043 non-null   object 
 2   SeniorCitizen     7043 non-null   int64  
 3   Partner           7043 non-null   object 
 4   Dependents        7043 non-null   object 
 5   tenure            7043 non-null   int64  
 6   PhoneService      7043 non-null   object 
 7   MultipleLines     7043 non-null   object 
 8   InternetService   7043 non-null   object 
 9   OnlineSecurity    7043 non-null   object 
 10  OnlineBackup      7043 non-null   object 
 11  DeviceProtection  7043 non-null   object 
 12  TechSupport       7043 non-null   object 
 13  StreamingTV       7043 non-null   object 
 14  StreamingMovies   7043 non-null   object 
 15  Contract          7043 non-null   object 
 16  PaperlessBilling  7043 non-null   object 
 17  PaymentMethod     7043 non-null   object 
 18  MonthlyCharges    7043 non-null   float64
 19  TotalCharges      7043 non-null   object 
 20  Churn             7043 non-null   object 
dtypes: float64(1), int64(2), object(18)
memory usage: 1.1+ MB

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

df['TotalCharges'] = df['TotalCharges'].apply(lambda x: pd.to_numeric(x, errors='coerce')).dropna()

Encodage des variables catégorielles

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

cat_features = df.drop(['customerID','TotalCharges','MonthlyCharges','SeniorCitizen','tenure'],axis=1)

cat_features.head()

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.

Regardons maintenant les types

df_cat.info()

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 7043 entries, 0 to 7042
Data columns (total 16 columns):
 #   Column            Non-Null Count  Dtype
---  ------            --------------  -----
 0   gender            7043 non-null   int64
 1   Partner           7043 non-null   int64
 2   Dependents        7043 non-null   int64
 3   PhoneService      7043 non-null   int64
 4   MultipleLines     7043 non-null   int64
 5   InternetService   7043 non-null   int64
 6   OnlineSecurity    7043 non-null   int64
 7   OnlineBackup      7043 non-null   int64
 8   DeviceProtection  7043 non-null   int64
 9   TechSupport       7043 non-null   int64
 10  StreamingTV       7043 non-null   int64
 11  StreamingMovies   7043 non-null   int64
 12  Contract          7043 non-null   int64
 13  PaperlessBilling  7043 non-null   int64
 14  PaymentMethod     7043 non-null   int64
 15  Churn             7043 non-null   int64
dtypes: int64(16)
memory usage: 880.5 KB

On va maintenant fusionner avec le dataframe qui était déjà numérique

num_features = df[['customerID','TotalCharges','MonthlyCharges','SeniorCitizen','tenure']]
finaldf = pd.merge(num_features, df_cat, left_index=True, right_index=True)

Oversampling

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.

#y_train
X_test.info()

<class 'pandas.core.frame.DataFrame'>
Index: 2321 entries, 2481 to 5585
Data columns (total 19 columns):
 #   Column            Non-Null Count  Dtype  
---  ------            --------------  -----  
 0   TotalCharges      2321 non-null   float64
 1   MonthlyCharges    2321 non-null   float64
 2   SeniorCitizen     2321 non-null   int64  
 3   tenure            2321 non-null   int64  
 4   gender            2321 non-null   int64  
 5   Partner           2321 non-null   int64  
 6   Dependents        2321 non-null   int64  
 7   PhoneService      2321 non-null   int64  
 8   MultipleLines     2321 non-null   int64  
 9   InternetService   2321 non-null   int64  
 10  OnlineSecurity    2321 non-null   int64  
 11  OnlineBackup      2321 non-null   int64  
 12  DeviceProtection  2321 non-null   int64  
 13  TechSupport       2321 non-null   int64  
 14  StreamingTV       2321 non-null   int64  
 15  StreamingMovies   2321 non-null   int64  
 16  Contract          2321 non-null   int64  
 17  PaperlessBilling  2321 non-null   int64  
 18  PaymentMethod     2321 non-null   int64  
dtypes: float64(2), int64(17)
memory usage: 362.7 KB

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

Reconstruire le venv de python pour corriger les problème de dépendances

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.

Reconstruire l’environnement venv

Mettez vous dans le répertoire projet

# windows
python -m venv venv

Linux/MacOs
python3 -m venv 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

pip install -r requirements

Une application en ReactJS pour interagir avec le smart contract

Cet article est la suite de l’article sur la mise en place d’un projet blockchain avec Hardhat.

Dans cet article , nous allon sconstgruire une application ReactJs pour interagir avec le smart contract.

Mise en place de l’application ReactJS. N’oubliez pas d’installer ehters.js

npx create-react-app my-token-frontend
cd my-token-frontend
npm install ethers

Création de l’interface de contrat

//   src/ContractABI.js

// This file contains the ABI (Application Binary Interface) for your smart contract
export const CONTRACT_ADDRESS = "0x5FbDB2315678afecb367f032d93F642f64180aa3"; // Replace with your actual contract address

export const CONTRACT_ABI = [
  // Read functions
  "function name() view returns (string)",
  "function symbol() view returns (string)",
  "function decimals() view returns (uint8)",
  "function totalSupply() view returns (uint256)",
  "function balanceOf(address) view returns (uint256)",
  "function allowance(address owner, address spender) view returns (uint256)",
  
  // Write functions
  "function transfer(address to, uint amount) returns (bool)",
  "function approve(address spender, uint256 amount) returns (bool)",
  "function transferFrom(address from, address to, uint256 amount) returns (bool)",
  "function mint(address to, uint256 amount)",
  
  // Events
  "event Transfer(address indexed from, address indexed to, uint amount)",
  "event Approval(address indexed owner, address indexed spender, uint256 value)"
];

Création du composant App

//src/App.js

import React, { useState, useEffect } from 'react';
import { ethers } from 'ethers';
import { CONTRACT_ADDRESS, CONTRACT_ABI } from './ContractABI';
import './App.css';

function App() {
  const [account, setAccount] = useState('');
  const [signer, setSigner] = useState(null);
  const [contract, setContract] = useState(null);
  const [tokenName, setTokenName] = useState('');
  const [tokenSymbol, setTokenSymbol] = useState('');
  const [balance, setBalance] = useState('0');
  const [transferTo, setTransferTo] = useState('');
  const [transferAmount, setTransferAmount] = useState('');
  const [mintTo, setMintTo] = useState('');
  const [mintAmount, setMintAmount] = useState('');
  const [isConnected, setIsConnected] = useState(false);
  const [isLoading, setIsLoading] = useState(false);
  const [errorMessage, setErrorMessage] = useState('');
  const [successMessage, setSuccessMessage] = useState('');

  useEffect(() => {
    // Check if already connected
    checkConnection();

    // Listen for account changes
    if (window.ethereum) {
      window.ethereum.on('accountsChanged', (accounts) => {
        if (accounts.length > 0) {
          connectWallet();
        } else {
          setIsConnected(false);
          setAccount('');
        }
      });
    }
  }, []);

  const checkConnection = async () => {
    if (window.ethereum) {
      try {
        const provider = new ethers.BrowserProvider(window.ethereum);
        const accounts = await provider.listAccounts();
        
        if (accounts.length > 0) {
          connectWallet();
        }
      } catch (error) {
        console.error("Error checking connection:", error);
      }
    }
  };

  const connectWallet = async () => {
    setErrorMessage('');
    setSuccessMessage('');
    setIsLoading(true);

    try {
      if (!window.ethereum) {
        setErrorMessage("MetaMask is not installed. Please install MetaMask to use this application.");
        setIsLoading(false);
        return;
      }

      // Request account access
      const provider = new ethers.BrowserProvider(window.ethereum);
      await provider.send("eth_requestAccounts", []);
      
      const signer = await provider.getSigner();
      const address = await signer.getAddress();
      
      const tokenContract = new ethers.Contract(CONTRACT_ADDRESS, CONTRACT_ABI, signer);
      
      setAccount(address);
      setSigner(signer);
      setContract(tokenContract);
      setIsConnected(true);

      // Get token info
      const name = await tokenContract.name();
      const symbol = await tokenContract.symbol();
      setTokenName(name);
      setTokenSymbol(symbol);
      
      // Get balance
      updateBalance(tokenContract, address);
      
      setSuccessMessage("Wallet connected successfully!");
    } catch (error) {
      console.error("Connection error:", error);
      setErrorMessage("Failed to connect wallet: " + error.message);
    } finally {
      setIsLoading(false);
    }
  };

  const updateBalance = async (tokenContract, address) => {
    try {
      const balance = await tokenContract.balanceOf(address);
      setBalance(ethers.formatEther(balance));
    } catch (error) {
      console.error("Error getting balance:", error);
    }
  };

  const handleTransfer = async (e) => {
    e.preventDefault();
    setErrorMessage('');
    setSuccessMessage('');
    setIsLoading(true);

    try {
      if (!ethers.isAddress(transferTo)) {
        throw new Error("Invalid recipient address");
      }

      const amount = ethers.parseEther(transferAmount);
      const tx = await contract.transfer(transferTo, amount);
      
      await tx.wait();
      
      setSuccessMessage(`Successfully transferred ${transferAmount} ${tokenSymbol} to ${transferTo}`);
      setTransferAmount('');
      
      // Update balance
      updateBalance(contract, account);
    } catch (error) {
      console.error("Transfer error:", error);
      setErrorMessage("Transfer failed: " + error.message);
    } finally {
      setIsLoading(false);
    }
  };

  const handleMint = async (e) => {
    e.preventDefault();
    setErrorMessage('');
    setSuccessMessage('');
    setIsLoading(true);

    try {
      if (!ethers.isAddress(mintTo)) {
        throw new Error("Invalid recipient address");
      }

      const amount = ethers.parseEther(mintAmount);
      const tx = await contract.mint(mintTo, amount);
      
      await tx.wait();
      
      setSuccessMessage(`Successfully minted ${mintAmount} ${tokenSymbol} to ${mintTo}`);
      setMintAmount('');
      
      // Update balance if minted to self
      if (mintTo.toLowerCase() === account.toLowerCase()) {
        updateBalance(contract, account);
      }
    } catch (error) {
      console.error("Mint error:", error);
      setErrorMessage("Mint failed: " + error.message);
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <div className="App">
      <header className="App-header">
        <h1>{tokenName || 'MyToken'} Interface</h1>
        
        {!isConnected ? (
          <button onClick={connectWallet} disabled={isLoading}>
            {isLoading ? 'Connecting...' : 'Connect Wallet'}
          </button>
        ) : (
          <div className="connected-container">
            <p>Connected Account: {account}</p>
            <p>Balance: {balance} {tokenSymbol}</p>
            
            <div className="card">
              <h2>Transfer Tokens</h2>
              <form onSubmit={handleTransfer}>
                <div className="form-group">
                  <label>Recipient Address:</label>
                  <input 
                    type="text" 
                    value={transferTo} 
                    onChange={(e) => setTransferTo(e.target.value)}
                    placeholder="0x..."
                    required
                  />
                </div>
                <div className="form-group">
                  <label>Amount:</label>
                  <input 
                    type="text" 
                    value={transferAmount} 
                    onChange={(e) => setTransferAmount(e.target.value)}
                    placeholder="0.0"
                    required
                  />
                </div>
                <button type="submit" disabled={isLoading}>
                  {isLoading ? 'Processing...' : 'Transfer'}
                </button>
              </form>
            </div>
            
            <div className="card">
              <h2>Mint Tokens (Owner Only)</h2>
              <form onSubmit={handleMint}>
                <div className="form-group">
                  <label>Recipient Address:</label>
                  <input 
                    type="text" 
                    value={mintTo} 
                    onChange={(e) => setMintTo(e.target.value)}
                    placeholder="0x..."
                    required
                  />
                </div>
                <div className="form-group">
                  <label>Amount:</label>
                  <input 
                    type="text" 
                    value={mintAmount} 
                    onChange={(e) => setMintAmount(e.target.value)}
                    placeholder="0.0"
                    required
                  />
                </div>
                <button type="submit" disabled={isLoading}>
                  {isLoading ? 'Processing...' : 'Mint'}
                </button>
              </form>
            </div>
          </div>
        )}
        
        {errorMessage && <p className="error-message">{errorMessage}</p>}
        {successMessage && <p className="success-message">{successMessage}</p>}
      </header>
    </div>
  );
}

export default App;

Un peu de style

//  src/App.css

.App {
  text-align: center;
  font-family: Arial, sans-serif;
}

.App-header {
  background-color: #282c34;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;
  font-size: calc(10px + 1vmin);
  color: white;
  padding: 20px;
}

button {
  background-color: #61dafb;
  border: none;
  color: #282c34;
  padding: 10px 20px;
  text-align: center;
  text-decoration: none;
  display: inline-block;
  font-size: 16px;
  margin: 10px 2px;
  cursor: pointer;
  border-radius: 4px;
  font-weight: bold;
}

button:disabled {
  background-color: #cccccc;
  cursor: not-allowed;
}

.connected-container {
  width: 100%;
  max-width: 800px;
}

.card {
  background-color: #3a3f4b;
  border-radius: 8px;
  padding: 20px;
  margin: 20px 0;
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}

.form-group {
  margin-bottom: 15px;
  text-align: left;
}

label {
  display: block;
  margin-bottom: 5px;
}

input {
  width: 100%;
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 16px;
}

.error-message {
  color: #ff6b6b;
  background-color: rgba(255, 107, 107, 0.1);
  padding: 10px;
  border-radius: 4px;
  margin: 10px 0;
}

.success-message {
  color: #51cf66;
  background-color: rgba(81, 207, 102, 0.1);
  padding: 10px;
  border-radius: 4px;
  margin: 10px 0;
}

Lancement du front

npm start

Configuration de MetaMask pour utiliser la blockchain locale

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

Account #10: 0xBcd4042DE499D14e55001CcbB24a551F3b954096 (10000TH)
Private Key: 0xf214f2b2cd398c806f84e317254e0f0b801d0643303237da22a48e01628897

On va vérifier que le compte 10 a bien reçu les tokens. Pour ce faire on va aller dans la shell de Hardhat

npx hardhat console --network localhost

Mettre en place un projet blockchain Ethereum avec Hardhat

Qu’est ce que Hardhat?

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

npm install @openzeppelin/contracts

Création d’un script de déploiement

script/deploy.js

const hre = require("hardhat");

async function main() {
    const [deployer] = await ethers.getSigners();
    console.log("Deploying contracts with the account:", deployer.address);

    const MyToken = await ethers.getContractFactory("MyToken");
    const myToken = await MyToken.deploy(deployer.address);

    await myToken.waitForDeployment();
    console.log("MyToken deployed to:", await myToken.getAddress());
}

main()
    .then(() => process.exit(0))
    .catch((error) => {
        console.error(error);
        process.exit(1);
    });

Création d’un fichier de test

// 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.

// Get the contract factory
const MyToken = await ethers.getContractFactory("MyToken");

// Connect to your deployed contract (replace with your actual contract address)
const myToken = await MyToken.attach("0x5FbDB2315678afecb367f032d93F642f64180aa3");

// Call read functions
const name = await myToken.name();
console.log("Token name:", name);

const totalSupply = await myToken.totalSupply();
console.log("Total supply:", totalSupply.toString());

// Call write functions (transactions)
const [owner, addr1] = await ethers.getSigners();
const mintTx = await myToken.mint(addr1.address, ethers.parseEther("100"));
await mintTx.wait();
console.log("Minted 100 tokens to:", addr1.address);

// Check balance
const balance = await myToken.balanceOf(addr1.address);
console.log("Balance:", ethers.formatEther(balance));

Je vous invite à jouer avec les lignes de commande pour afficher les contenus des variables par exemple.

Interaction avec un script nodeJS

//  interact.js

const hre = require("hardhat");

async function main() {
  // Replace with your deployed contract address
  const contractAddress = "0x5FbDB2315678afecb367f032d93F642f64180aa3";
  
  const MyToken = await ethers.getContractFactory("MyToken");
  const myToken = MyToken.attach(contractAddress);
  
  console.log("Token name:", await myToken.name());
  console.log("Token symbol:", await myToken.symbol());
  
  const [owner, addr1] = await ethers.getSigners();
  console.log("Owner address:", owner.address);
  
  // Get owner balance
  const ownerBalance = await myToken.balanceOf(owner.address);
  console.log("Owner balance:", ethers.formatEther(ownerBalance));
  
  // Mint tokens to addr1
  console.log("Minting 100 tokens to:", addr1.address);
  const mintTx = await myToken.mint(addr1.address, ethers.parseEther("100"));
  await mintTx.wait();
  
  // Get addr1 balance
  const addr1Balance = await myToken.balanceOf(addr1.address);
  console.log("Address 1 balance:", ethers.formatEther(addr1Balance));
}

main()
  .then(() => process.exit(0))
  .catch(error => {
    console.error(error);
    process.exit(1);
  });

Pour faire tourner le script vous devez l’invoquer avec Hardhat

npx hardhat run scripts/interact.js --network localhost

Quoi d’autres après?

Il serait intéressant de faire une application ReactJS pour interagir avec le smart contract.

Linux Parallel pour exécuter des process en parallèle et gagner du temps

Pourquoi utiliser parallel?

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

Linux Nohup pour détacheer un process shell

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

nohup ping google.com > output.txt  &

Savoir manipuler les MIME-Type en informatique

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>



Installer NginX en tant que reverse proxy

C’est quoi Nginx?

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.

On a vu comment installer Nginx en tant que serveur web, mais on va faire du reverse proxy dans cet article.

C’est quoi un 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.

Lier le nouveau fichier de configuration

sudo ln -s /etc/nginx/sites-available/reverse-proxy /etc/nginx/sites-enabled/

#test nginx
sudo nginx -t

#redémarre Nginx
sudo systemctl restart nginx

Tester le reverse proxy, exemple avec Python

Créer un répertoire, faire un fichier index.html, et lancer le serveur web Python depuis ce même répertoire. Par exemple /var/www

<html>
<head>
  <title>NGINX backend</title>
</head>
<body>
  <h1>ça marche !</h1>
</body>
</html>

#lancer le serveur

python3 -m http.server 8000

Il se peut que Python ne soit pas installé, pour Debian faites : sudo apt install python3

Test avec Curl de cette page et accès à cette même page par un navigateur.

curl http://14.35.123.45/index.html

Cas d’une application NodeJS sur le port 3000

On applique la même stratégie, on modifie le fichier pour remplacer le port 8000 par 3000.

Explication reverse proxy avec Nginx en vidéo

Le requêtes cross domaine faciles avec le formation JSONP (JSON with Padding)

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.

Un exemple simple de JSONP

Le fichier front end jsonpfront.php :

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>JSON P</title>
</head>

<body>
    <div id="demo"></div>
</body>
<script>
    function myFunc(myObj) {
        document.getElementById("demo").innerHTML = myObj.name;
    }
</script>

<script src="jsonp.php"></script>

</html>

Le fichier back jsonp.php.

<?php
$myJSON = '{ "name":"John", "age":30, "city":"New York" }';

echo "myFunc(".$myJSON.");";
?>

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?

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>JSON P</title>
</head>

<body>
    <div id="demo"></div>
    <a href=# onclick="clickButton();">Click</a>
</body>
<script>
    function myFunc(myObj) {
        document.getElementById("demo").innerHTML = myObj.name;
    }

    function clickButton() {
        let s = document.createElement("script");
        s.src = "jsonp.php?x=Yvon";
        document.body.appendChild(s);
    }
</script>


</html>

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.

<?php
header("Content-Type: application/json; charset=UTF-8");
$obj = json_decode($_GET["x"], false);
$outp = ["name" => $_GET["x"], "age" => 30, "city" => "New York"];
echo "myFunc(" . json_encode($outp) . ")";

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





Comment ferait on en AJAX?

Dans la jungle des NPM, NPX,NVM, YARN et PNPM

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.

Installer le shell zsh sur Ubuntu

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

git clone https://github.com/zsh-users/zsh-syntax-highlighting.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting

redémarrez votre shell et commencez à taper la commande echo $0. (qui affiche le nom du shell courant) et observez le changement de couleur.

Introduction à tmux terminal multiplexer

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
  • Changer de panneau : CTRL + B puis flèche
  • Sortir d’une session tmux : CTRL +B puis D (ou d)
  • Démarrer tmux avec la dernière session : tmux a

Installation de tmux

apt install tmux    -  sur Debian Ubuntu
brew install tmux    - macOs

Lancement tmux et commandes de base

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.

Pour fermer la fenêtre courante : CTRL + D puis &

source :

Comment partager votre site web en local sans utiliser Ngrok, ni localtunnel

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 !

Utiliser netcat pour écouter des messages locaux et distants

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

Découverte des named pipe dans Linux.

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.

Prendre en main UFW Uncomplicated Firewall sous Linux Ubuntu

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

https://www.vps-mart.com/blog/how-to-configure-firewall-with-ufw-on-ubuntu

// 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	

Configurer le firewall Linux iptables

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)

Configurer fail2ban

Qu’est ce que c’est fail2ban?

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 :

sudo apt-get install rsyslog

Débuter avec Docker avec un déploiement simple

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>
Retour en haut