SheevaPlug

Neuf ans

Sunday, 04 August 2019
|
Écrit par
Grégory Soutadé

Gâteau anniversaire 9 ans

Neuf ans. Voilà un chiffre qui commence à faire mal sur le coin de la tête ! Comme tout événement ou jalon qui ponctue notre vie et nous fait réaliser des rétrospectives, on se dit qu'il y a 9 ans, après l'achat et la configuration du SheevaPlug et bien ... j'avais neuf ans de moins !

Sans refaire tout l'historique, qui sera sûrement abordé l'année prochaine, revenons sur les événements marquants de cette année. Quelque chose qui était attendu depuis longtemps : l'arrivée d'une connexion fibre. Certes, pour l'usage personnel, l'ancienne connexion ADSL avec ses 13Mb/s descendants était suffisante pour assurer une navigation fluide, mais concernant le débit montant (donc les connexions des utilisateurs externes) ça ramait dès qu'il y avait quelques photos. Voilà un problème de résolu. Pour autant, je ne vais pas abandonner mon architecture actuelle de pages statiques pré compressées, ni mettre des tas de scripts inutiles et consommateurs de ressources aussi bien côté client que côté serveur. Vive l'internet libre, léger et sans pub ! Néanmoins, et c'est une grosse déception sur ce point, il n'y a toujours pas d'IPv6.

Buster, la version 10 de Debian, est sortie début juillet. La mise à jour du serveur était donc au menu. Elle s'est parfaitement déroulée avec très peu de problèmes concernant les fichiers de configuration et sans coupure majeure. Ce fut l'occasion de se rendre compte que Bouygues Telecom avait mis à jour sa box en ajoutant une règle qui bloque le port entrant 25... Ce qui signifie que les mails en @soutade.fr ne fonctionnaient plus depuis environ 8 mois.

Les statistiques pour cette année (entre parenthèses, les années précédentes) :

  • 19 articles publiés (22, 30, 31, 34, 49, 50, 60, 60)
  • 9 270 visites (9 580, 9 510, 23 800, 21 300, 25 000, 12 000, 18 000, 9 000)
  • 15.3 Go de données envoyées (12.5, 17, 17,9, 9, 5.5, 2.7, 2.5)
  • 22 230 pages affichées (19 887, 20 180, 26 700)

Le fameux top 10 qui cumule 41% des pages affichées (en gras, consultation principalement pour les images. Entre parenthèses, l'année de publication) :

Certes, les chiffres sont encore en baisses par rapport aux années précédentes. Je ne compte pas pour autant fermer les services. D'autant plus que certains sujets sont encore très actifs ! On continue donc avec la philosophie Debian : une nouvelle sortie quand c'est prêt ! Normalement, une nouvelle version de gPass devrait sortir courant de cette année. Il faut dire que ce projet rencontre un certain succès avec quelques 175 utilisateurs actifs ce jour. Il y a même eu un pic à 275 en début d'année. La concurrence est pourtant féroce, avec en prime deux logiciels de gestion de mots de passes homonymes (un pour Gnome et un pour Google).

Let's encrypt certificate renewal with Gandi LiveDNS API

Tuesday, 02 April 2019
|
Écrit par
Grégory Soutadé

It's now one year I use Let's Encrypt TLS wildcard certificates. Until now, all was fine, but since the beginning of 2019, there is two domains on my certificate : soutade.fr and *.soutade.fr and (maybe due to my certificate generation) I need to perform two challenges for renewal : HTTP (http01) and DNS (dns01).

So, I wrote a Python script that performs both :

#!/usr/bin/env python3
#-*- encoding: utf-8 -*-

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

#
# Handle certificate renewal using HTTP and DNS challenges
# DNS challenge performed by Gandi Live v5 API
#

import requests
import os
import argparse
import shutil

# Config
API_KEY = "YOUR-KEY"
LIVEDNS_API = "https://dns.api.gandi.net/api/v5/"
ACME_RECORD = '_acme-challenge'
ACME_CHALLENGE_PATH = '/var/www/.well-known/acme-challenge'

headers = {
    'X-Api-Key': API_KEY,
}

CERTBOT_TOKEN = os.environ.get('CERTBOT_TOKEN', None)
CERTBOT_VALIDATION = os.environ.get('CERTBOT_VALIDATION', None)
DOMAIN = os.environ.get('CERTBOT_DOMAIN', None)

optparser = argparse.ArgumentParser(description='Letsencrypt challenge for Gandi v5 API')
optparser.add_argument('-c', '--cleanup', dest='cleanup',
                       action="store_true", default=False,
                       help='Cleanup chanllenge')

options = optparser.parse_args()     

if options.cleanup:
    print('Cleanup')
    if os.path.exists(ACME_CHALLENGE_PATH):
        shutil.rmtree(ACME_CHALLENGE_PATH)
else:
    if CERTBOT_TOKEN and CERTBOT_VALIDATION:
        print('Build HTTP authentication')
        # Create token file for web server
        if not os.path.exists(ACME_CHALLENGE_PATH):
            os.makedirs(ACME_CHALLENGE_PATH)
        token_path = os.path.join(ACME_CHALLENGE_PATH, CERTBOT_TOKEN)

        with open(token_path, 'w') as token:
            token.write(CERTBOT_VALIDATION)
        exit(0)

response = requests.get(LIVEDNS_API + "zones", headers=headers)

target_zone = None
if (response.ok):
    zones = response.json()
    for zone in zones:
        if zone['name'] == DOMAIN:
            target_zone = zone
            break
else:
    response.raise_for_status()
    exit(1)

if not target_zone:
    print('Any zone found for domain %s' % (DOMAIN))
    exit(1)

domain_records_href = target_zone['zone_records_href']

# Get TXT record
response = requests.get(domain_records_href + "/" + ACME_RECORD, headers=headers)

# Delete record if it exists
if (response.ok):
    requests.delete(domain_records_href + "/" + ACME_RECORD, headers=headers)

if options.cleanup:
    exit(0)

print('Build DNS authentication')
record = {
    "rrset_name": ACME_RECORD,
    "rrset_type": "TXT",
    "rrset_ttl": 300,
    "rrset_values": [CERTBOT_VALIDATION],
    }

response = requests.post(domain_records_href,
                         headers=headers, json=record)

if (response.ok):
    print("DNS token created")
else:
    print("Something went wrong")
    response.raise_for_status()
    exit(1)

A downloadable version is available here

Crontab

In /etc/crontab :

0  1   1 * *   root   certbot renew  --manual -n --manual-public-ip-logging-ok --manual-auth-hook /root/gandi_letsencrypt.py --manual-cleanup-hook /root/letsencrypt_token_cleanup.sh

Aditionnals Scripts

Where /root/letsencrypt_token_cleanup.sh is

#!/bin/bash

/root/gandi_letsencrypt.py --cleanup

And in /etc/letsencrypt/renewal-hooks/post/ :

#!/bin/bash

service nginx restart

Errors

If you get a 404 error with nginx, you may add this line to ensure it will not delegate treatment in other part (or send it to another webserver) :

        location /.well-known/acme-challenge/ {
        }

Huit ans

Saturday, 04 August 2018
|
Écrit par
Grégory Soutadé

Gâteau 8 ans

Une bougie de plus pour le blog ! L'occasion de faire un nouveau bilan. Comme chaque année, le temps passe vite et le temps libre devient une ressource rare, dispersée entre le travail professionnel et les multiples activités. Du coup, ce sont les projets informatiques qui prennent du retard. J'ai particulièrement en tête la sortie de Pannous qui a pas mal traînée.

Contrairement à ce que j'avais pré annoncé l'année passée, il n'y a toujours pas de support IPv6, mais ça devrait être réglé d'ici la fin de l'année ! Si on regarde les statistiques, elles sont plutôt stables, avec un pic de connexions en janvier grâce mon vieil article fétiche : 1 message service reçu. La bande passante a bien diminuée car moins de photos en première page. Certes, il y a un petit peu moins d'articles publiés, mais certains sont vraiment conséquents et, même s'ils n'apparaissent pas dans le top 10, j'en suis assez fier. En parlant de chiffres et de fierté, à l'occasion du correctif 0.8.2 de gPass, j'ai jeté un oeil (par hasard) sur les statistiques d'utilisation de l'addon Chrome. Et quelle ne fût pas ma surprise de voir une moyenne de 168 utilisateurs quotidiens + une douzaine sur Firefox ! C'est un chiffre impressionant pour un projet sans pub (mis à part sur le blog), auto hébergé, sans instance centrale et à l'esthétique un peu vieillot (c'est à ça que l'on reconnaît la sécurité).

Les statistiques pour cette année (entre parenthèses, les années précédentes) :

  • 22 articles publiés (30, 31, 34, 49, 50, 60, 60)
  • 9 580 visites (9 510, 23 800, 21 300, 25 000, 12 000, 18 000, 9 000)
  • 12.5 Go de données envoyées (17, 17,9, 9, 5.5, 2.7, 2.5)
  • 19 887 pages affichées (20 180, 26 700)

Le fameux top 10 qui cumule pour cette année 38% des visites (en gras, consultation principalement pour les images. Entre parenthèses, l'année de publication) :

On notera deux entrées : l'astuce pour windows et la revue du Canon EOS M10 de janvier 2017 !

Quid de l'avenir ? Pas d'annonce cette année, ni de projet particulier à mener. Je pense avoir atteint un rythme de croisière et j'espère faire aussi bien (voir mieux) l'année prochaine !

Gandi Live DNS v5

Thursday, 26 April 2018
|
Écrit par
Grégory Soutadé

La toute récente sortie de Pannous a été l'occasion de créer un nouveau sous-domaine pour héberger le service. Service qui comporte une partie d'authentification, donc obligation de passer par une communication sécurisée (SSL/TLS). Autrefois, la chose était plus aisée, puisque je pouvais générer mes propres certificats et notamment des certificats "wildcards", donc valides pour tous les sous-domaines.

Sauf que je suis passé à Let's Encrypt. Il a donc fallu attendre la sortie de la version 2 du protocole (qui a eu du retard) afin de bénéficier de cette fonctionnalité. Surtout, qu'au passage, le paquet Debian (backport) de certbot a été cassé, ce qui m'a forcé à revenir à une version encore plus ancienne.

Bref, les choses sont maintenants stables et déployées sur les serveurs respectifs. Petit problème néanmoins, la génération d'un certificat wildcard par Let's Encrypt requiert l'ajout d'une entrée DNS (comme challenge). Fatalité, le DNS de Gandi a lui aussi évolué pour passer en version 5. Avec pour principal avantage une mise à jour immédiate des entrées DNS (là où il fallait plusieurs minutes/heures auparavant). Autre nouveauté : l'API Gandi change de format. On oublie l'ancien XML-RPC (ce qui était pratique avec des bindings Python déjà tout faits), pour passer au REST (un peu moins formel).

Mélangeons tout ça pour obtenir un joli cocktail, dont la recette nous est donnée par Sébastien Blaisot qui, pour nous simplifier la vie, a créé des scripts de génération de certificats wildcards via Let's Encrypt. Le code est disponible sur GitHub et supporte le logiciel bind (en local), l'API OVH et la nouvelle API Gandi. Il ne reste plus qu'à cloner le dépôt et lancer la commande magique :

cd certbot-dns-01-authenticators/gandi-livedns
certbot certonly --manual --server https://acme-v02.api.letsencrypt.org/directory\
 --manual-auth-hook $PWD/auth.py --manual-cleanup-hook $PWD/cleanup.py -d '*.soutade.fr'

Et voilà un joli certificat tout frais !

Du coup, je me suis grandement inspiré de son code pour mettre à jour mon script de DNS fallback (serveur de secours via redirection DNS). Avec, en prime, un passage en Python 3 ! À terme, il faudra que j'ajoute le support IPv6.

#!/usr/bin/env python3
# -*- encoding: utf-8 -*-

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import requests
import json
import re

# Config
domain="soutade.fr"
API_KEY = "YOUR-KEY"
livedns_api = "https://dns.api.gandi.net/api/v5/"
dyndns_url = 'http://checkip.dyndns.com/'
headers = {
    'X-Api-Key': API_KEY,
}
A_RECORD_NAME="@" # Record of A type

# Get current IP
current_ip = ''
response = requests.get(dyndns_url)
if response.ok:
    pattern = re.compile('[^:]*(\d+\.\d+\.\d+\.\d+)')
    result = pattern.search(response.text, 0)
    if result == None:
        print("No IP found")
        exit(1) 
    else:
        current_ip = result.group(0).strip()
else:
    print("Connexion error")
    response.raise_for_status()
    exit(1)

print('Your Current IP is %s' % (current_ip))

# Retrieve domains address
response = requests.get(livedns_api + "domains", headers=headers)

if (response.ok):
    domains = response.json()
else:
    response.raise_for_status()
    exit(1)

domain_index = next((index for (index, d) in enumerate(domains) if d["fqdn"] == domain), None)

if domain_index == None:
    # domain not found
    print("The requested domain " + domain + " was not found in this gandi account")
    exit(1)

domain_records_href = domains[domain_index]["domain_records_href"]

# Get recorded IP
response = requests.get(domain_records_href + "/" + A_RECORD_NAME + "/A", headers=headers)

if (response.ok):
    record = response.json()
else:
    print("Failed to look for recorded IP")
    response.raise_for_status()
    exit(1)

print('Old IP : %s' % (record['rrset_values'][0]))

if current_ip != record['rrset_values'][0]:
    record['rrset_values'][0] = current_ip

    # Put updated IP
    response = requests.put(domain_records_href + "/" + A_RECORD_NAME + "/A", headers=headers, json=record)

    if (response.ok):
        print("IP updated")
    else:
        print("something went wrong")
        response.raise_for_status()
        exit(1)

    exit(34) # IP updated return !!

exit(0)

Une version téléchargable est disponible ici.

Pannous v0.1

Thursday, 26 April 2018
|
Écrit par
Grégory Soutadé

Logo Pannous

Il y a quelques temps déjà, Mozilla a décidé d'abandonner son format et ses API pour les extensions de son navigateur (Firefox) afin de tout migrer vers les WebExtensions. Ces dernières reprennent en très grande partie les caractéristiques de son concurrent Chrome. Cet événement, à priori négatif, m'a permis d'améliorer la sécurité de mon extension gPass et de factoriser grandement le code. Malheureusement, la migration de la version 0.7 (ancien format) à la version 0.8 n'est pas automatique (contrairement à Chrome) et beaucoup d'utilisateurs se sont perdus en chemin. C'est à ce moment-là que je me suis rendu compte qu'il me manquait un lien avec les utilisateurs de manière générale.

Il est vrai que ma forge inDefero n'est plus maintenue depuis maintenant 8 ans, qu'il n'y a pas de fonctionnalité "sociale" ou de "distribution" pour prévenir d'éventuelles nouveautés. Pour autant, je n'ai pas envie de passer sur une autre plateforme (notamment Github) et je ne trouve aucun remplaçant aussi ergonomique. La plupart des candidats ne se contentant que d'imiter la plateforme propriétaire majeure. L'idée, toute simple, est donc de créer des listes de diffusion projets (mailing list ou announce list).

La référence dans le domaine est GNU mailman. Je pourrais y faire les mêmes reproches que pour les forges : compliqué à configurer, moche, lourd. Alors que mes besoins sont assez légers. Du coup, je me suis lancé dans l'écriture d'un petit moteur de liste de diffusion. À priori, rien de compliqué : un petit script pour la gestion des mails et une gestion assez simples des listes/groupes/utilisateurs (ajout, modification, suppressions, droits). Le tout en PHP : simple, rapide et avec tous les modules nécessaires. En effet, j'ai décidé d'abandonner mon Django favoris, car, s'il aurait pu répondre à tous mes besoins, il est extrêmement lent pour mon petit serveur.

Au début, j'ai récupéré un peu de code d'un ancien projet avec un framework perso (simpliste). Mais en complexifiant un tout petit peu le projet, je me suis dit qu'il était inutile de ré inventer la roue. J'ai donc fait le tour des frameworks PHP (des plus lourds et connus aux plus légers). C'est assez impressionnant de voir que certains font des centaines de méga pour une application de moins de 2MO... Après comparaison, il s'avère qu'ils adoptent tous la même philosophie HMVC et l'on peut passer de l'un à l'autre assez rapidement. Après avoir longtemps lorgné sur CakePHP, j'ai finalement décidé de me lancer en FuelPHP. J'y retrouve toutes les fonctionnalités que j'attends, en plus d'être léger et rapide.

Bref, tout ça pour annoncer la création de Pannous pour PHP Annoucement System. L'objectif est d'être simple à installer, configurer, utiliser. On y retrouve deux fonctionnalités inédites :

  • Plusieurs niveaux de droits pour l'émission d'un mail sur la liste (écrivains, lecteurs, utilisateurs, tout le monde), avec la possibilité de vérifier la signature GPG.
  • API Rest pour l'inscription/désinscription d'une liste (avec génération automatique de code HTML/AJAX).

Pour le reste, c'est du classique : gestion des utilisateurs, des groupes, des listes, inscription/désinscription en ligne ou par mail...

Le projet est disponible sur ma Forge sous licence GPL v3.

Naturellement, j'ai créé une liste pour Pannous : pannous-announce@soutade.fr