Espace tutos dev’ & informatique 🐧

Restriction avancée d’adresse IP avec Apache

Publié le
Article sur la restriction des adresses IP des visiteurs sous serveur Apache

Introduction

Parfois, il est nécessaire de mettre en ligne un site internet mais de ne le rendre accessible que depuis certains emplacements. Par exemple, un mini-site évènementiel de marque pourrait n’être accessible que depuis les boutiques affiliées à cette dernière.

Dans cet article, je propose une solution simple et avancée de restriction des adresses IP des visiteurs pour le serveur web Apache. Elle ne demande que la mise en place d’un fichier .htaccess et ne dépend que du module mod_rewrite, la plupart des hébergeurs devraient donc la supporter.

On va procéder du plus facile (renvoyer une erreur 403 Forbidden) jusqu’au plus complet (permettre aux visiteurs de saisir un identifiant et un mot de passe).

Note : le code a été testé sur un environnement Apache 2.4 chez l’hébergeur o2switch. N’oubliez pas de toujours faire une sauvegarde avant de modifier les fichiers de votre site !

Liste blanche des adresses IP

Tout d’abord, voyons comment n’autoriser l’accès qu’à certaines adresses IP. Le code ci-dessous bloque l’accès aux visiteurs non-autorisés en affichant une erreur 403 Forbidden. Il est à placer aussi haut que possible dans un fichier .htaccess à la racine du site ou du dossier à protéger.

<IfModule mod_rewrite.c>
  RewriteEngine On

  # Liste blanche des adresses IPs
  # Autoriser le localhost :
  RewriteCond expr "-R '127.0.0.0/8'" [OR]
  # Autoriser votre serveur (remplacer avec son adresse) :
  RewriteCond expr "-R '109.234.164.136'" [OR]
  # Autoriser les visiteurs :
  RewriteCond expr "-R '1.2.3.4'" [OR]
  RewriteCond expr "-R '192.168.1.0/24'"
  # Variable d'état d'autorisation
  RewriteRule .* - [E=visitor_is_whitelisted:1]

  # Blocage 403 des visiteurs interdits
  RewriteCond %{ENV:visitor_is_whitelisted} !1
  RewriteRule .* - [L,F]

</IfModule>

On utilise les expressions ap_expr d’Apache, qui permettent soit de matcher une adresse IP exacte (ex : 1.2.3.4), soit de matcher un ensemble d’adresses IP (ex : 192.168.1.0/24). Il ne faut pas oublier d’autoriser le localhost ainsi que le serveur lui-même, notamment si des scripts cron doivent être exécutés.

Si une directive ErrorDocument 403 est présente, la page d’erreur prévue sera affichée au visiteur (voir la documentation). Dans le cas contraire, c’est la page d’erreur 403 par défaut qui sera utilisée :

Page d'erreur 403 de la restriction IP du serveur Apache
Le serveur affiche une erreur 403 pour les visiteurs non-autorisés

 

Liste blanche des pages web

Si vous avez besoin de permettre l’accès à certaines ressources aux visiteurs non-autorisés, il est possible de placer leurs URLs sur une liste blanche. On pense notamment aux favicons, mentions légales et formulaires de demande de support. Il suffit de rajouter quelques lignes au fichier .htaccess précédent, comme ci-dessous.

<IfModule mod_rewrite.c>
  RewriteEngine On

  # Liste blanche des adresses IPs
  # Autoriser le localhost :
  RewriteCond expr "-R '127.0.0.0/8'" [OR]
  # Autoriser votre serveur (remplacer avec son adresse) :
  RewriteCond expr "-R '109.234.164.136'" [OR]
  # Autoriser les visiteurs :
  RewriteCond expr "-R '1.2.3.4'" [OR]
  RewriteCond expr "-R '192.168.1.0/24'"
  # Variable d'état d'autorisation
  RewriteRule .* - [E=visitor_is_whitelisted:1]

  # Liste blanche des URLs
  # Autoriser ces URLs :
  RewriteCond %{REQUEST_URI} ^/pageautorisee\.html$ [OR]
  RewriteCond %{REQUEST_URI} ^/dossierautorise/
  # Variable d'etat d'autorisation
  RewriteRule .* - [E=visitor_is_whitelisted:1]

  # Blocage 403 des visiteurs interdits
  RewriteCond %{ENV:visitor_is_whitelisted} !1
  RewriteRule .* - [L,F]

</IfModule>

Il est possible de matcher à la fois des fichiers et des dossiers, ce qui permet de donner facilement accès aux ressources publiques du site.

On note que l’on travaille avec des expressions régulières, et qu’il faut donc échapper les caractères spéciaux tels que le point.

Afficher une page accès refusé

Plutôt que d’afficher une bête erreur 403, pourquoi ne pas rediriger les visiteurs non-autorisés vers une page d’erreur personnalisée ? Quelque chose comme ça :

Page d'accès refusé de la restriction IP du serveur Apache
Page affichée aux visiteurs non-autorisés


Il faut d’abord créer la page web qui affichera le message d’erreur. Insérer et personnalisez le code suivant dans un fichier accesrefuse.html à la racine du site.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Accès refusé</title>
    <meta name="robots" content="noindex, nofollow">
    <style>
      body{ font-size: 16px; line-height: 1.6; font-weight: 400; font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif; text-align: center; color: #111 }
      img{ display:inline-block; max-width:100%; min-height:1px; height:auto }
      section{ padding: 100px 50px }
    </style>
  </head>
  <body>
    <section>
      <div>
        <img alt="Accès refusé" src="data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg viewBox='0 0 1000 1000' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath transform='matrix(5.1965 0 0 5.3726 -738.86 -644.43)' d='m324.45 213.01a86.051 83.23 0 1 1 -172.1 0 86.051 83.23 0 1 1 172.1 0z' fill='none' stroke='%23f00' stroke-width='20'/%3E%3Cpath d='m166.83 166.83 666.33 666.33' fill='none' stroke='%23f00' stroke-width='100'/%3E%3C/svg%3E" width="1000" height="1000" style="width: 250px; margin-bottom: 20px" />
      </div>
      <h1>Accès refusé :-(</h1>
      <p>Nous sommes désolés, mais l'accès au service n'est pas autorisé depuis votre adresse de connexion.<br />Veuillez vous connecter à un point d'accès autorisé et réessayer.<br /><br /><a href="/">Cliquez ici pour essayer à nouveau.</a></p>
    </section>
  </body>
</html>

Il faut que toutes les ressources utilisées soient en liste blanche dans le fichier .htaccess. Il est préférable d’éviter d’utiliser des ressources externes à la page ; par exemple, dans le cas des images, on peut les intégrer directement dans le code HTML grâce au format data-uri.

Ne pas oublier la balise meta robots contenant la valeur noindex ! Il serait dommage de voir les moteurs de recherche indexer la page de message d’erreur.

On peut finalement reprendre le code du fichier .htaccess, ajouter la page d’accès refusé à la liste blanche et remplacer l’erreur 403 par une redirection 302 :

<IfModule mod_rewrite.c>
  RewriteEngine On

  # Liste blanche des adresses IPs
  # Autoriser le localhost :
  RewriteCond expr "-R '127.0.0.0/8'" [OR]
  # Autoriser votre serveur (remplacer avec son adresse) :
  RewriteCond expr "-R '109.234.164.136'" [OR]
  # Autoriser les visiteurs :
  RewriteCond expr "-R '1.2.3.4'" [OR]
  RewriteCond expr "-R '192.168.1.0/24'"
  # Variable d'état d'autorisation
  RewriteRule .* - [E=visitor_is_whitelisted:1]

  # Liste blanche des URLs
  # Autoriser la page acces refuse
  RewriteCond %{REQUEST_URI} ^/accesrefuse\.html$ [OR]
  # Autoriser ces URLs :
  RewriteCond %{REQUEST_URI} ^/pageautorisee\.html$ [OR]
  RewriteCond %{REQUEST_URI} ^/dossierautorise/
  # Variable d'etat d'autorisation
  RewriteRule .* - [E=visitor_is_whitelisted:1]

  # Redirection 302 des visiteurs interdits vers la page acces refuse
  RewriteCond %{ENV:visitor_is_whitelisted} !1
  RewriteRule .* %{REQUEST_SCHEME}://%{HTTP_HOST}/accesrefuse.html [L,R=302,QSD]

</IfModule>

❓  Si vous vous demandez pourquoi je n’ai pas utilisé une directive ErrorDocument 403, la réponse est que la redirection 302 permet de gérer le cas où un site dispose déjà d’un ErrorDocument mais que l’on veut afficher une page différente. Vous pouvez tout à fait utiliser un ErrorDocument de votre côté 😉

Identifiant et mot de passe

La mise en liste blanche peut ne pas s’avérer suffisante. Par exemple, prenons le cas d’un outil web d’entreprise, qui ne doit être disponible qu’au personnel de cette dernière. On peut mettre en liste blanche les adresses IP des locaux de l’entreprise, mais comment permettre au personnel en déplacement d’avoir aussi un accès complet à l’outil ?

L’identification par login et mot de passe des visiteurs externes est une solution. Ca consiste à utiliser une page de connexion qui ressemble à ceci :

Page d'accès externe de la restriction IP du serveur Apache
Page d’identification des visiteurs non-autorisés


Commençons d’abord par créer la page de saisie des identifiants du visiteur. Créez un fichier accesexterne.html à la racine du site, et placez y le code suivant, en le personnalisant si besoin. N’oubliez pas de limiter autant que possible l’utilisation de ressources externes, que vous devrez placer en liste blanche le cas échéant. La balise meta robots noindex est aussi indispensable.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Accès externe</title>
    <meta name="robots" content="noindex, nofollow">
    <style>
      body{ font-size: 16px; line-height: 1.6; font-weight: 400; font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif; text-align: center; color: #111 }
      img{ display:inline-block; max-width:100%; min-height:1px; height:auto }
      section{ padding: 30px 30px }
      @media screen and (max-width: 600px){
        input[type=text], input[type=password]{ width: 100%; margin-bottom: 15px }
      }
    </style>
  </head>
  <body>
    <section>
      <div>
        <img alt="Accès externe" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' version='1.1' viewBox='0 0 256 256'%3E%3Cg transform='translate(128 128) scale(0.72 0.72)' style=''%3E%3Cg style='stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: none; fill-rule: nonzero; opacity: 1;' transform='translate(-175.05 -175.05000000000004) scale(3.89 3.89)' %3E%3Cpath d='M 0 68.798 v 11.914 c 0 1.713 1.401 3.114 3.114 3.114 h 0 c 3.344 0 4.805 -2.642 4.805 -2.642 L 8.14 29.281 l 2.739 -2.827 l 72.894 -2.977 v -1.482 c 0 -2.396 -1.942 -4.338 -4.338 -4.338 H 50.236 c -1.15 0 -2.254 -0.457 -3.067 -1.27 l -8.943 -8.943 c -0.813 -0.813 -1.917 -1.27 -3.067 -1.27 H 4.338 C 1.942 6.174 0 8.116 0 10.512 v 7.146 v 2.332 V 68.798' style='stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(224,173,49); fill-rule: nonzero; opacity: 1;' transform=' matrix(1 0 0 1 0 0) ' stroke-linecap='round' /%3E%3Cpath d='M 3.114 83.826 L 3.114 83.826 c 1.713 0 3.114 -1.401 3.114 -3.114 V 27.81 c 0 -2.393 1.94 -4.333 4.333 -4.333 h 75.107 c 2.393 0 4.333 1.94 4.333 4.333 v 51.684 c 0 2.393 -1.94 4.333 -4.333 4.333 C 85.667 83.826 3.114 83.826 3.114 83.826 z' style='stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(255,200,67); fill-rule: nonzero; opacity: 1;' transform=' matrix(1 0 0 1 0 0) ' stroke-linecap='round' /%3E%3Cpath d='M 57.675 48.711 h -2.651 v -7.515 c 0 -4.424 -3.6 -8.023 -8.023 -8.023 c -4.424 0 -8.024 3.599 -8.024 8.023 v 7.515 h -2.651 c -0.829 0 -1.5 0.672 -1.5 1.5 v 21.351 c 0 0.828 0.671 1.5 1.5 1.5 h 21.35 c 0.828 0 1.5 -0.672 1.5 -1.5 V 50.211 C 59.175 49.383 58.503 48.711 57.675 48.711 z M 41.976 41.196 c 0 -2.77 2.254 -5.023 5.024 -5.023 c 2.77 0 5.023 2.253 5.023 5.023 v 7.515 H 41.976 V 41.196 z M 56.175 70.062 h -18.35 V 51.711 h 18.35 V 70.062 z' style='stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(184,53,53); fill-rule: nonzero; opacity: 1;' transform=' matrix(1 0 0 1 0 0) ' stroke-linecap='round' /%3E%3Cpath d='M 52.425 59.215 c 0 -2.99 -2.434 -5.424 -5.425 -5.424 s -5.425 2.434 -5.425 5.424 c 0 2.47 1.662 4.556 3.925 5.209 v 2.057 c 0 0.828 0.672 1.5 1.5 1.5 s 1.5 -0.672 1.5 -1.5 v -2.057 C 50.763 63.771 52.425 61.685 52.425 59.215 z M 47 61.64 c -1.337 0 -2.425 -1.088 -2.425 -2.425 s 1.088 -2.424 2.425 -2.424 s 2.425 1.087 2.425 2.424 S 48.337 61.64 47 61.64 z' style='stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(184,53,53); fill-rule: nonzero; opacity: 1;' transform=' matrix(1 0 0 1 0 0) ' stroke-linecap='round' /%3E%3C/g%3E%3C/g%3E%3C/svg%3E" width="256" height="256" style="width: 150px; margin-bottom: 20px" />
      </div>
      <h1>Authentification accès externe</h1>
      <p>L'accès au service n'est possible que depuis un point d'accès autorisé ou aux utilisateurs externes identifiés.</p>
      <p>Utilisez le formulaire ci-dessous pour confirmer votre identité.</p>
      <form action="" style="margin-bottom: 20px;">
        <input type="text" name="login" placeholder="Identifiant" />
        <input type="password" name="password" placeholder="Mot de passe" />
        <input type="submit" value="Connexion" />
      </form>
      <p>Vous pouvez aussi vous reconnecter depuis un point d'accès autorisé et réessayer.<br /><a href="/">Cliquez ici pour essayer à nouveau.</a></p>
    </section>
    <script>
      // Variables personnalisables
      var REDIRECT_URL = "/"; // Personnaliser la redirection apres la soumission du formulaire
      var COOKIE_DURATION = 86400; // Personnaliser la durée du cookie d'authentification (secondes)

      // sha1() from webtoolkit.info
      function sha1(r){function o(r,o){return r<<o|r>>>32-o}function e(r){var o,e="";for(o=7;o>=0;o--)e+=(r>>>4*o&15).toString(16);return e}var t,a,h,n,C,c,f,d,A,u=new Array(80),g=1732584193,i=4023233417,s=2562383102,S=271733878,m=3285377520,p=(r=function(r){r=r.replace(/\r\n/g,"\n");for(var o="",e=0;e<r.length;e++){var t=r.charCodeAt(e);t<128?o+=String.fromCharCode(t):t>127&&t<2048?(o+=String.fromCharCode(t>>6|192),o+=String.fromCharCode(63&t|128)):(o+=String.fromCharCode(t>>12|224),o+=String.fromCharCode(t>>6&63|128),o+=String.fromCharCode(63&t|128))}return o}(r)).length,l=new Array;for(a=0;a<p-3;a+=4)h=r.charCodeAt(a)<<24|r.charCodeAt(a+1)<<16|r.charCodeAt(a+2)<<8|r.charCodeAt(a+3),l.push(h);switch(p%4){case 0:a=2147483648;break;case 1:a=r.charCodeAt(p-1)<<24|8388608;break;case 2:a=r.charCodeAt(p-2)<<24|r.charCodeAt(p-1)<<16|32768;break;case 3:a=r.charCodeAt(p-3)<<24|r.charCodeAt(p-2)<<16|r.charCodeAt(p-1)<<8|128}for(l.push(a);l.length%16!=14;)l.push(0);for(l.push(p>>>29),l.push(p<<3&4294967295),t=0;t<l.length;t+=16){for(a=0;a<16;a++)u[a]=l[t+a];for(a=16;a<=79;a++)u[a]=o(u[a-3]^u[a-8]^u[a-14]^u[a-16],1);for(n=g,C=i,c=s,f=S,d=m,a=0;a<=19;a++)A=o(n,5)+(C&c|~C&f)+d+u[a]+1518500249&4294967295,d=f,f=c,c=o(C,30),C=n,n=A;for(a=20;a<=39;a++)A=o(n,5)+(C^c^f)+d+u[a]+1859775393&4294967295,d=f,f=c,c=o(C,30),C=n,n=A;for(a=40;a<=59;a++)A=o(n,5)+(C&c|C&f|c&f)+d+u[a]+2400959708&4294967295,d=f,f=c,c=o(C,30),C=n,n=A;for(a=60;a<=79;a++)A=o(n,5)+(C^c^f)+d+u[a]+3395469782&4294967295,d=f,f=c,c=o(C,30),C=n,n=A;g=g+n&4294967295,i=i+C&4294967295,s=s+c&4294967295,S=S+f&4294967295,m=m+d&4294967295}return(A=e(g)+e(i)+e(s)+e(S)+e(m)).toLowerCase()}

      // Soumission du formulaire
      var form = document.querySelector("form");
      form.addEventListener("submit", function(event){
        event.preventDefault();
        var login = form.querySelector("[name=login]").value;
        var password = form.querySelector("[name=password]").value;
        document.cookie = "accesexterne_login=" + encodeURIComponent(sha1(login)) + "; path=/; max-age=" + COOKIE_DURATION + "; samesite=lax";
        document.cookie = "accesexterne_password=" + encodeURIComponent(sha1(password)) + "; path=/; max-age=" + COOKIE_DURATION + "; samesite=lax";
        window.location = REDIRECT_URL;
      });
    </script>
  </body>
</html>

La page enregistre 2 cookies dans le navigateur lors de la soumission du formulaire. L’un contient le hash SHA-1 de l’identifiant, l’autre le hash du mot de passe. Le visiteur est ensuite redirigé vers l’adresse configurée dans le code source, par défaut la racine du site.

La validation des cookies s’effectue au niveau du fichier .htaccess. Nous devons placer la page d’accès externe en liste blanche, et vérifier si les cookies contiennent les bons hash d’identifiant et mot de passe.

<IfModule mod_rewrite.c>
  RewriteEngine On

  # Liste blanche des adresses IPs
  # Autoriser le localhost :
  RewriteCond expr "-R '127.0.0.0/8'" [OR]
  # Autoriser votre serveur (remplacer avec son adresse) :
  RewriteCond expr "-R '109.234.164.136'" [OR]
  # Autoriser les visiteurs :
  RewriteCond expr "-R '1.2.3.4'" [OR]
  RewriteCond expr "-R '192.168.1.0/24'"
  # Variable d'état d'autorisation
  RewriteRule .* - [E=visitor_is_whitelisted:1]

  # Liste blanche des URLs
  # Autoriser la page acces externe
  RewriteCond %{REQUEST_URI} ^/accesexterne\.html$ [OR]
  # Autoriser la page acces refuse
  RewriteCond %{REQUEST_URI} ^/accesrefuse\.html$ [OR]
  # Autoriser ces URLs :
  RewriteCond %{REQUEST_URI} ^/pageautorisee\.html$ [OR]
  RewriteCond %{REQUEST_URI} ^/dossierautorise/
  # Variable d'etat d'autorisation
  RewriteRule .* - [E=visitor_is_whitelisted:1]

  # Autorisation par cookie
  # Autoriser l'utilisateur 1
  RewriteCond expr "%{HTTP_COOKIE} -strmatch '*accesexterne_login=%{sha1:Login1}*'"
  RewriteCond expr "%{HTTP_COOKIE} -strmatch '*accesexterne_password=%{sha1:Password1}*'"
  RewriteRule .* - [E=visitor_is_whitelisted:1]
  # Autoriser l'utilisateur 2
  RewriteCond expr "%{HTTP_COOKIE} -strmatch '*accesexterne_login=%{sha1:Login2}*'"
  RewriteCond expr "%{HTTP_COOKIE} -strmatch '*accesexterne_password=%{sha1:Password2}*'"
  RewriteRule .* - [E=visitor_is_whitelisted:1]

  # Redirection 302 des visiteurs interdits vers la page acces refuse
  RewriteCond %{ENV:visitor_is_whitelisted} !1
  RewriteRule .* %{REQUEST_SCHEME}://%{HTTP_HOST}/accesrefuse.html [L,R=302,QSD]

</IfModule>

A vous de remplacer Login1, Password1, Login2, Password2 par des identifiants et mots de passe plus adaptés, et de rajouter des utilisateurs si nécessaire.

On utilise la fonction sha1 intégrée à Apache qui permet de calculer des hash directement depuis les fichiers htaccess.
Si vous ne souhaitez pas inscrire en clair les identifiants dans le code, vous pouvez remplacer les appels %{sha1:Login1} par la valeur du hash lui-même.

Vous pouvez également remplacer la redirection vers la page d’accès refusé en une redirection vers la page de saisie des identifiants. Pour ma part, je préfère garder secrète l’adresse de la page de saisie.

Il ne vous reste plus qu’à transmettre aux visiteurs autorisés l’URL de la page d’accès externe, ainsi que leurs identifiants et mots de passe.

Pour aller plus loin

Avant de conclure, voyons quelques réflexions et pistes d’amélioration pour notre bloqueur d’accès.

🔒  Le protocole HTTPS est obligatoire pour l’identification par cookie. Sinon, n’importe quel intermédiaire entre le visiteur et le serveur peut dérober les cookies et accéder au site.

💥  Attention à l’impact sur la charge. Si chaque page, chaque script, chaque image du site doit passer par la validation IP + URL + cookie, il se pourrait que le serveur soit impacté.


💡  Idée évolution #1 : et si on ne filtrait que certaines URLs ? Il suffit de placer le code de filtrage dans une directive <If>. A vous de jouer, ce n’est pas très compliqué 😉

💡  Idée évolution #2 : placer les adresses IP, les URLs en liste blanche et les identifiants dans des fichiers séparés. Ca facilitera la vie du sysadmin.

💡  Idée évolution #3 : fluidifier l’arrivée des visiteurs en les redirigeant automatiquement vers la page qu’ils souhaitaient afficher après leur authentification. Ca leur évitera d’avoir à reparcourir le site depuis l’accueil.

Conclusion

Voila, nous disposons d’un mécanisme facile à déployer et à personnaliser pour bloquer l’accès d’un site internet aux visiteurs non-autorisés. Il devrait être compatible avec WordPress, Drupal, Symfony et consorts. Il devrait également se montrer assez performant, car il est exécuté directement par le serveur Apache, sans avoir à passer par PHP.

Si cet article vous a plu ou si vous avez besoin d’un coup de main pour votre projet web, n’hésitez pas à m’écrire depuis la page contact du site.

Bon code à vous, à la prochaine !

Références

Documentation mod_rewrite – Apache HTTP Server
Les expressions ap_expr – Apache HTTP Server
La directive ErrorDocument – Apache HTTP Server
Les cookies HTTP – Mozilla Developers

Photo de profil de Loïc Burtin Code Colibri développeur web à Dijon

A propos de l'auteur

Loïc Burtin

Développeur web indépendant, avec une tendance à couper les plumes en quatre pour que les sites se chargent plus rapidement.

Partagez cet article  =>