Sécurité d'un site web en Go (Golang)

Dans le monde Go il n’est pas très courant d’utiliser des frameworks web comme on pourrait le voir dans d’autres langages. Cela donne certes de la liberté, mais un avantage majeur des frameworks web est qu’ils imposent certaines bonnes pratiques en terme de sécurité, notamment pour les néophytes.

Si, comme moi, vous développez des sites ou API en Go sans framework, voyons ici quels éléments de sécurité vous devez garder à l’esprit.

CSRF

Les attaques de type Cross-site Requests Forgery (CSRF) concernent les parties du site protégées par mot de passe faisant l’usage de formulaires. Les utilisateurs authentifiés (via un cookie de session dans leur navigateur) risquent, s’ils se rendent sur un site malicieux, de poster des informations sur un formulaire protégé à leur insu. Pour éviter ce risque, il faut que chaque formulaire intègre un champ caché contenant un token CSRF grâce auquel le serveur pourra vérifier l’authenticité de la requête.

Utilisons Gorilla Toolkit pour cela. Commencez par intégrer le middleware CSRF. Vous pouvez le faire soit pour tout le site:

package main

import (
    "net/http"

    "github.com/gorilla/csrf"
    "github.com/gorilla/mux"
)

func main() {
    r := mux.NewRouter()

    http.ListenAndServe(":8000",
        csrf.Protect([]byte("32-byte-long-auth-key"))(r))
}

Soit pour certaines pages seulement:

package main

import (
    "net/http"

    "github.com/gorilla/csrf"
    "github.com/gorilla/mux"
)

func main() {
    r := mux.NewRouter()
    csrfMiddleware := csrf.Protect([]byte("32-byte-long-auth-key"))

    protectedPageRouter := r.PathPrefix("/protected-page").Subrouter()
    protectedPageRouter.Use(csrfMiddleware)
    protectedPageRouter.HandleFunc("", protectedPage).Methods("POST")

    http.ListenAndServe("8080", r)
}

Puis passez le token CSRF à votre template:

func protectedPage(w http.ResponseWriter, r *http.Request) {
    var tmplData = ContactTmplData{CSRFField: csrf.TemplateField(r)}
    tmpl.Execute(w, tmplData)
}

Et enfin intégrez le champs caché {{.CSRFField}} à votre template.

CORS

Les attaques de type Cross-origin Resource Sharing (CORS) consistent à envoyer des informations vers un site malicieux en se rendant sur un site sain. Pour éviter cela le site sain doit empêcher ses utilisateurs d’émettre des requêtes asynchrones (XHR) vers un autre site que lui. Bonne nouvelle : c’est le comportement par défaut appliqué par tous les navigateurs modernes ! Mauvaise nouvelle : cela peut créer un faux positif qui fait que si vous souhaitez, depuis vote page web, consommer une API se trouvant sur un autre domaine que le domaine d’origine, ou sur un port différent, les requêtes seront bloquées par le navigateur. C’est souvent un casse-tête pour les débutants en développement d’API.

Afin de mettre certains domaines sur liste blanche et ainsi éviter le problème ci-dessus, vous pouvez utiliser la bibliothèque github.com/rs/cors comme suit :

package main

import (
    "net/http"

    "github.com/rs/cors"
)

func main() {
    c := cors.New(cors.Options{
        AllowedOrigins: []string{"http://my-whitelisted-domain"},
    })
    handler = c.Handler(handler)

    http.ListenAndServe(":8080", handler)
}

HTTPS

Passer le site en HTTPS est un élément de sécurité incontournable. Je considère ici que vous utilisez le serveur HTTP built-in de Go. Si ce n’est pas le cas (parce que vous utilisez par exemple Nginx ou Apache) vous pouvez sauter cette section.

Obtenir un A sur SSLLabs.com

Afin d’obtenir la meilleure note de sécurité sur SSLLabs (signe que le certificat est parfaitement configuré et ainsi éviter potentiellement les erreurs de sécurité sur certains clients web), il faut proscrire l’utilisation de SSL et utiliser TLS 1.0 comme version minimum. Pour cela on utilise la bibliothèque crypto/tls, et pour servir les requêtes on utilise http.ListenAndServeTLS:

package main

import (
    "crypto/tls"
    "net/http"
)

func main() {
    config := &tls.Config{MinVersion: tls.VersionTLS10}
    server := &http.Server{Addr: ":443", Handler: r, TLSConfig: config}
    server.ListenAndServeTLS(tlsCertPath, tlsKeyPath)
}

Rediriger HTTP vers HTTPS

C’est une bonne pratique de forcer les requêtes HTTP vers HTTPS. Il suffit de les rediriger comme suit via une goroutine dédiée :

package main

import (
    "crypto/tls"
    "net/http"
)

// httpsRedirect redirects http requests to https
func httpsRedirect(w http.ResponseWriter, r *http.Request) {
    http.Redirect(
        w, r,
        "https://"+r.Host+r.URL.String(),
        http.StatusMovedPermanently,
    )
}

func main() {
    go http.ListenAndServe(":80", http.HandlerFunc(httpsRedirect))

    config := &tls.Config{MinVersion: tls.VersionTLS10}
    server := &http.Server{Addr: ":443", Handler: r, TLSConfig: config}
    server.ListenAndServeTLS(tlsCertPath, tlsKeyPath)
}

Renouvellement des certificats Let’s Encrypt

Let’s Encrypt est aujourd’hui la façon la plus répandue de provisionner les certificats TLS (car gratuite) mais pas nécessairement la plus facile. Une fois Let’s Encrypt installé et les premiers certificats provisionnés, se pose la question du renouvellement des certificats avec Certbot. Puisque Certbot ne s’intègre pas automatiquement au server HTTP de Go, il est nécessaire d’utiliser la version standard de certbot (par exemple celle-là pour Ubuntu 18.04), puis de couper brièvement (quelques secondes) le serveur de production pendant le renouvellement des certificats (afin d’éviter un conflit sur les ports 80 ou 443). Cela peut se faire en modifiant la commande de renouvellement lancée par le cron Certbot (dans /etc/cron.d/certbot). Sur Ubuntu Certbot utilise aussi le timer systemd (en priorité par rapport au cron) donc modifier le fichier de configuration /lib/systemd/system/certbot.service est préférable :

[Unit]
Description=Certbot
Documentation=file:///usr/share/doc/python-certbot-doc/html/index.html
Documentation=https://letsencrypt.readthedocs.io/en/latest/
[Service]
Type=oneshot
# Proper command to stop server before renewal and restart server afterwards
ExecStart=/usr/bin/certbot -q renew --pre-hook "command to stop go server" --post-hook "command to start go server"
PrivateTmp=true

Une alternative est d’utiliser une bibliothèque Go dédiée au renouvellement des certificats au sein même de votre programme appelée x/crypto/acme/autocert. Personnellement j’aime moins cette option car, même si elle ne crée aucune interruption de service contrairement à ma solution ci-dessus, elle oblige votre code à être fortement couplé à un type de renouvellement de certificats particulier (ACME).

XSS

Les attaques de type Cross-site scripting (XSS) consistent à exécuter du code Javascript dans le navigateur d’un utilisateur se rendant sur votre site, alors que ça ne devrait pas être le cas. Par exemple dans le cas d’un forum, si un utilisateur poste un message contenant du Javascript, et que vous affichez ce message à tous les utilisateurs sans filtre, ces derniers verront tous ce script s’exécuter dans leur propre navigateur. Pour éviter cela il faut “échapper” les chaînes de caractères avant de les afficher à l’utilisateur afin de les rendre inoffensives.

Bonne nouvelle : lorsque vous utilisez les templates via la bibliothèque html/template, Go sécurise les chaînes de caractères automatiquement. Attention à ne pas utiliser text/template qui elle n’échappe pas les caractères !

Injections SQL

Les attaques de type injection SQL consistent pour un utilisateur malicieux à entrer du code SQL dans un formulaire du site. Au moment de l’enregistrement ou de l’affichage des informations en base de donnée, ce code malicieux peut faire des dégâts.

Encore une fois, bonne nouvelle : cette attaque se contourne naturellement en utilisant correctement les bibliothèques SQL classiques. Par exemple en utilisant database/sql les injections SQL sont automatiquement échappées lorsque vous utilisez les mots clés $ ou ?. Voici le cas d’une requête SQL pour PostgreSQL bien écrite :

db.Exec("INSERT INTO users(name, email) VALUES($1, $2)",
  "Julien Salinas", "julien@salinas.com")

Personnellement j’utilise l’ORM optimisé pour PostgreSQL github.com/go-pg/pg. Une requête bien écrite dans ce cas serait :

user := &User{
    name:       "Julien Salinas",
    email:      "julien@salinas.com",
}
err = db.Insert(user)

Directory Listing

Le directory listing est le fait de pouvoir afficher tout le contenu d’un répertoire statique sur le site. Cela peut révéler des documents que vous ne souhaitez pas afficher à l’utilisateur s’ils n’ont pas l’url exacte. Le directory listing est activé par défaut si vous utilisez la bibliothèque standard http.FileServer. J’explique dans cet article comment le neutraliser.

Conclusion

Voici un petit aperçu des éléments essentiels de sécurité pour votre site en Go.

Il peut être une bonne idée d’utiliser un outil permettant de paramétrer facilement différents éléments essentiels relatifs à la sécurité. Je trouve que github.com/unrolled/secure est une excellente bibliothèque à cet égard. Elle permet de facilement paramétrer les redirections HTTPS, gérer CORS, mais aussi le filtrage d’hôtes autorisés et pas mal de choses encore plus pointues.

J’espère que ces bases seront utiles à certains !

Also available in English
Développer et déployer un site entier en Go (Golang)

À mon avis Go (Golang) est un excellent choix de langage pour le développement web :

  • il permet les requêtes non bloquantes et rend la concurrence facile
  • il facilite le test du code et le déploiement puisqu’il ne demande d’installer aucun environnement d’exécution ou de dépendances
  • il n’exige pas d’installer un serveur HTTP front-end tel qu’Apache ou Nginx puisqu’il en embarque déjà un excellent dans sa bibliothèque standard
  • il ne vous force pas à utiliser un framework web puisque tous les outils requis pour le développement web sont déjà disponibles dans la bibliothèque standard

Il y a encore quelques années, le manque de bibliothèques et tutoriels autour de Go étaient un problème, mais aujourd’hui ce n’est plus le cas. Je vais vous montrer étape par étape comment construire un site web en Go et le déployer sur votre serveur Linux de A à Z.

Les bases

Imaginons que vous souhaitiez développer une page HTML basique appelée love-mountains. Comme vous le savez peut-être déjà, la génération de love-mountains se fait dans une fonction, et il vous faut lancer le serveur web avec une route pointant vers cette fonction. C’est une bonne pratique que d’utiliser des templates HTML lorsque l’on fait du développement web alors générons notre page via un template basique ici. C’est aussi une bonne pratique que d’importer des paramètres tels que le chemin vers le dossier de template depuis des variables d’environnement pour une meilleure flexibilité.

Voici votre code Go :

package main

import (
    "html/template"
    "net/http"
)

// Get path to template directory from env variable
var templatesDir = os.Getenv("TEMPLATES_DIR")

// loveMountains renders the love-mountains page after passing some data to the HTML template
func loveMountains(w http.ResponseWriter, r *http.Request) {
    // Build path to template
    tmplPath := filepath.Join(templatesDir, "love-mountains.html")
    // Load template from disk
    tmpl := template.Must(template.ParseFiles(tmplPath))
    // Inject data into template
    data := "La Chartreuse"
    tmpl.Execute(w, data)
}

func main() {
    // Create route to love-mountains web page
    http.HandleFunc("/love-mountains", loveMountains)
    // Launch web server on port 80
    http.ListenAndServe(":80", nil)
}

La récupération de données dynamiques depuis un template se fait facilement via {{.}} ici. Voici votre template love-mountains.html:

<h1>I Love Mountains<h1>
<p>The mountain I prefer is {{.}}</p>

HTTPS

De nos jours, mettre en place l’HTTPS sur votre site est devenu quasiment obligatoire. Comment passer votre site Go HTTPS ?

Lier les certificats TLS

Avant tout, générez votre certificat et votre clé privée au format .pem. Vous pouvez parfaitement les générer vous-même avec openssl (mais vous allez vous retrouver avec un certificat auto-signé qui déclenchera une alerte de sécurité dans le navigateur), ou bien vous pouvez commander un certificat auprès d’une tierce partie de confiance comme Let’s Encrypt. Personnellement, j’utilise Let’s Encrypt et Certbot afin de générer les certificats et les renouveler automatiquement sur mes serveurs. Plus d’infos sur l’utilisation de Certbot ici.

Il vous faut alors dire à Go où se trouve votre certificat et votre clé privée. J’importe ces chemins depuis des variables d’environnement.

On utilise désormais la fonctionListenAndServeTLS au lieu de ListenAndServe :

[...]

// Load TLS cert info from env variables
var tlsCertPath = os.Getenv("TLS_CERT_PATH")
var tlsKeyPath = os.Getenv("TLS_KEY_PATH")

[...]

func main() {
    [...]
    // Serve HTTPS on port 443
    http.ListenAndServeTLS(":443", tlsCertPath, tlsKeyPath, nil)
}

Forcer les redirections HTTPS

Pour le moment nous avons un site web qui écoute sur les ports 80 et 443 à la fois. Il serait bien de rediriger automatiquement les utilisateurs du port 80 vers 443 avec une redirection 301. Il nous faut pour cela créer une nouvelle goroutine dédiée à la redirection de http:// vers https:// (même principe que ce que vous feriez dans la configuration d’un serveur front-end comme Nginx). Voici ce qu’il faut faire :

[...]

// httpsRedirect redirects HTTP requests to HTTPS
func httpsRedirect(w http.ResponseWriter, r *http.Request) {
    http.Redirect(
        w, r,
        "https://"+r.Host+r.URL.String(),
        http.StatusMovedPermanently,
    )
}

func main() {
    [...]
    // Catch potential HTTP requests and redirect them to HTTPS
    go http.ListenAndServe(":80", http.HandlerFunc(httpsRedirect))
    // Serve HTTPS on port 443
    http.ListenAndServeTLS(":443", tlsCertPath, tlsKeyPath, nil)
}

Les assets statiques

Servir les assets statiques (comme les images, les vidéos, les fichiers Javascript, les fichiers CSS,…) stockés sur le disque est relativement facile mais désactiver le directory listing relève un peu plus de la bidouille.

Servir les fichiers statiques depuis le disque

En Go, la façon la plus sécurisée de servir des fichiers depuis le disque est d’utiliser http.FileServer. Par exemple, disons que nous stockons les fichiers statiques dans un dossier sur le disque appelé static, et que nous voulons les servir à cette adresse : https://my-website/static. Voici comment il faut s’y prendre :

[...]
http.Handle("/", http.FileServer(http.Dir("static")))
[...]

Empêcher le directory listing

Par défaut,http.FileServer permet un directory listing sans limitation, ce qui signifie que https://my-website/static affichera tous vos fichiers statiques. Ce n’est pas ce que nous voulons pour des questions à la fois de sécurité et de propriété intellectuelle.

Désactiver le directory listing requiert la création d’un FileSystem custom. Créons une struct qui implémente l’interface http.FileSystem. Cette struct doit avoir une méthode Open() afin de satisfaire l’interface. Cette méthode Open() vérifie en premier si le chemin vers le fichier ou le répertoire existe, et si c’est le cas elle détermine s’il s’agit d’un fichier ou d’un répertoire. Si le chemin est un répertoire alors nous retournons une erreur file does not exist qui sera convertie en une page d’erreur HTTP 404 pour l’utilisateur final. De cette façon l’utilisateur ne peut pas savoir s’il a atteint un dossier qui existe vraiment ou non.

Une fois de plus, importons le chemin vers le dossier de fichiers statiques depuis une variable d’environnement.

[...]

// Get path to static assets directory from env variable
var staticAssetsDir = os.Getenv("STATIC_ASSETS_DIR")

// neuteredFileSystem is used to prevent directory listing of static assets
type neuteredFileSystem struct {
    fs http.FileSystem
}

func (nfs neuteredFileSystem) Open(path string) (http.File, error) {
    // Check if path exists
    f, err := nfs.fs.Open(path)
    if err != nil {
        return nil, err
    }

    // If path exists, check if is a file or a directory.
    // If is a directory, stop here with an error saying that file
    // does not exist. So user will get a 404 error code for a file or directory
    // that does not exist, and for directories that exist.
    s, err := f.Stat()
    if err != nil {
        return nil, err
    }
    if s.IsDir() {
        return nil, os.ErrNotExist
    }

    // If file exists and the path is not a directory, let's return the file
    return f, nil
}

func main() {
    [...]
    // Serve static files while preventing directory listing
    mux := http.NewServeMux()
    fs := http.FileServer(neuteredFileSystem{http.Dir(staticAssetsDir)})
    mux.Handle("/", fs)
    [...]
}

Exemple complet

Au final, voici à quoi ressemblerait votre site en entier :

package main

import (
    "html/template"
    "net/http"
    "os"
    "path/filepath"
)

var staticAssetsDir = os.Getenv("STATIC_ASSETS_DIR")
var templatesDir = os.Getenv("TEMPLATES_DIR")
var tlsCertPath = os.Getenv("TLS_CERT_PATH")
var tlsKeyPath = os.Getenv("TLS_KEY_PATH")

// neuteredFileSystem is used to prevent directory listing of static assets
type neuteredFileSystem struct {
    fs http.FileSystem
}

func (nfs neuteredFileSystem) Open(path string) (http.File, error) {
    // Check if path exists
    f, err := nfs.fs.Open(path)
    if err != nil {
        return nil, err
    }

    // If path exists, check if is a file or a directory.
    // If is a directory, stop here with an error saying that file
    // does not exist. So user will get a 404 error code for a file/directory
    // that does not exist, and for directories that exist.
    s, err := f.Stat()
    if err != nil {
        return nil, err
    }
    if s.IsDir() {
        return nil, os.ErrNotExist
    }

    // If file exists and the path is not a directory, let's return the file
    return f, nil
}

// loveMountains renders love-mountains page after passing some data to the HTML template
func loveMountains(w http.ResponseWriter, r *http.Request) {
    // Load template from disk
    tmpl := template.Must(template.ParseFiles("love-mountains.html"))
    // Inject data into template
    data := "Any dynamic data"
    tmpl.Execute(w, data)
}

// httpsRedirect redirects http requests to https
func httpsRedirect(w http.ResponseWriter, r *http.Request) {
    http.Redirect(
        w, r,
        "https://"+r.Host+r.URL.String(),
        http.StatusMovedPermanently,
    )
}

func main() {
    // http to https redirection
    go http.ListenAndServe(":80", http.HandlerFunc(httpsRedirect))

    // Serve static files while preventing directory listing
    mux := http.NewServeMux()
    fs := http.FileServer(neuteredFileSystem{http.Dir(staticAssetsDir)})
    mux.Handle("/", fs)

    // Serve one page site dynamic pages
    mux.HandleFunc("/love-mountains", loveMountains)

    // Launch TLS server
    log.Fatal(http.ListenAndServeTLS(":443", tlsCertPath, tlsKeyPath, mux))
}

Plus votre template love-mountains.html :

<h1>I Love Mountains<h1>
<p>The mountain I prefer is {{.}}</p>

Tester, déployer, et créer un démon avec systemd

Bâtir un process de test et de déploiement robuste et facile à la fois est très important pour l’efficacité d’un projet, et Go est vraiment d’une grande aide à cet égard. Go compile tout au sein d’un seul et unique exécutable, incluant toutes les dépendances (excepté les templates mais ces derniers ne sont pas réellement des dépendances et il est de toute façon plus sage de les garder à part pour une meilleure flexibilité). Go embarque aussi son propre serveur HTTP front-end, il n’est donc pas besoin d’installer un Nginx ou un Apache. Il est ainsi relativement facile de tester votre application en local et de vous assurer que cette dernière est équivalente à votre site web en production sur le serveur (ici nous n’abordons pas la question de la persistance des données bien entendu…). Ainsi nul besoin de mettre en place un système de conteneur comme Docker dans votre workflow pour compiler et déployer !

Tester

Pour tester votre application localement, compilez votre binaire Go et lancez-le avec les bonnes variables d’environnement de cette façon :

TEMPLATES_DIR=/local/path/to/templates/dir \
STATIC_ASSETS_DIR=/local/path/to/static/dir \
TLS_CERT_PATH=/local/path/to/cert.pem \
TLS_KEY_PATH=/local/path/to/privkey.pem \
./my_go_website

Et voilà ! Votre site tourne désormais dans votre navigateur à l’adresse https://127.0.0.1.

Déployer

Le déploiement consiste juste à copier votre binaire Go sur le serveur (ainsi que vos templates, assets statiques et certificats, le cas échéant). Un simple outil tel que scp est parfait pour cela. Vous pouvez aussi utiliser rsync en cas de besoin plus poussé.

Transformer votre app en démon avec systemd

Vous pourriez lancer votre site web sur le serveur en vous contentant de la commande ci-dessus, mais il est bien mieux de lancer votre site sous forme de service (démon) afin que votre système Linux le lance automatiquement au démarrage (en cas de redémarrage serveur) et essaie aussi de le redémarrer en cas de crash de l’application. Sur les distributions Linux modernes, la meilleure façon de s’y prendre est d’utiliser systemd, qui est l’outil par défaut dédié à la gestion des services système. Rien à installer donc !

Supposons que vous mettiez votre binaire Go dans /var/www sur votre serveur. Créez un nouveau fichier qui décrira votre service dans le répertoire systemd : /etc/systemd/system/my_go_website.service. Mettez maintenant le contenu suivant à l’intérieur :

[Unit]
Description=my_go_website
After=network.target auditd.service

[Service]
EnvironmentFile=/var/www/env
ExecStart=/var/www/my_go_website
ExecReload=/var/www/my_go_website
KillMode=process
Restart=always
RestartPreventExitStatus=255
Type=simple

[Install]
WantedBy=multi-user.target

La directive EnvironmentFile pointe vers un fichier env dans lequel vous pouvez mettre toutes vos variables d’environnement. systemd s’occuper de charger ces variables et de les passer à votre programme. J’ai placé le fichier dans /var/www mais n’hésitez pas à le placer autre part. Voici à quoi votre fichier env devrait ressembler :

TEMPLATES_DIR=/remote/path/to/templates/dir
STATIC_ASSETS_DIR=/remote/path/to/static/dir
TLS_CERT_PATH=/remote/path/to/cert.pem
TLS_KEY_PATH=/remote/path/to/privkey.pem

N’hésitez pas à vous documenter sur systemd pour plus de détails sur la configuration ci-dessus.

À présent :

  • lancez ce qui suit pour lier votre app à systemd : systemctl enable my_go_website
  • lancez ce qui suit pour démarrer votre site : systemctl start my_go_website
  • redémarrez avec : systemctl restart my_go_website
  • arrêtez avec : systemctl stop my_go_website

Remplacer Javascript par WebAssembly (Wasm)

Voici une petite section bonus pour ceux qui se sentent l’âme d’un aventurier !

Depuis la version 1.11 de Go vous pouvez désormais compiler Go vers WebAssembly (Wasm). Plus de détails ici. C’est vraiment cool puisque Wasm peut servir de substitut à JavaScript. En d’autres mots vous pouvez théoriquement remplacer JavaScript par Go à travers Wasm.

Wasm est supporté par les navigateurs modernes mais reste malgré tout relativement expérimental. Personnellement, je ne l’utiliserais que pour une preuve de concept pour le moment, mais à moyen terme cela pourrait devenir une excellente façon de développer en Go sur l’intégralité de votre stack. Patience donc !

Conclusion

Vous savez désormais comment développer un site web entier en Go, et le déployer sur un serveur Linux. Pas de serveur front-end à installer, pas de cauchemar à cause des dépendances, et des performances excellentes… C’était plutôt facile pas vrai ?

Si vous voulez apprendre à développer une Single Page App (SPA) avec Go et Vue.js, jetez un coup d’œil à mon autre post sur le sujet ici.

Also available in English
Torrengo: un CLI de recherche de torrents concurrent en Go

J’utilise le programme Torrench depuis un moment maintenant et je trouve que c’est un super outil en Python pour facilement rechercher des torrents en console (CLI). Je me suis dit que je pourrais essayer de développer un outil similaire en Go (Golang) puisque ce serait une bonne opportunité pour moi d’apprendre de nouvelles choses et que je pourrais introduire de la concurrence dans ce programme, ce qui augmenterait sensiblement les performances (Torrench est lent à cet égard).

Laissez-moi vous parler des fonctionnalités de ce nouveau programme ici, vous montrer comment ils gère la concurrence, et enfin vous expliquer comment j’ai organisé cette bibliothèque Go en vue d’une réutilisation par des tiers.

Fonctionnalités de Torrengo

Torrengo est un programme en ligne de commande (CLI) qui recherche les torrents (fichiers torrents et liens magnet) de façon concurrente depuis différents sites web. Rechercher des torrents a toujours été une catastrophe pour moi puisque ça signifiait à chaque fois lancer la même recherche sur de nombreux sites, faire face à des sites qui fonctionnent mal, et se retrouver inondé de pub scandaleuses… ce petit outil est là pour tout résoudre. Voici un aperçu des fonctionnalités clés de Torrengo au moment où j’écris ces lignes.

Un programme CLI simple

J’ai réalisé que je n’avais pas beaucoup d’expérience concernant l’écriture d’outils en ligne de commande, je me suis donc dit que ce petit projet serait une bonne façon d’améliorer mes compétences en la matière.

Les programmes CLI dédiés à des cas d’usage très spécifiques comme celui-là et avec très peu d’interaction utilisateur peuvent s’avérer être beaucoup plus clairs et légers qu’un GUI. Ils peuvent aussi facilement se brancher à d’autres programmes.

Vous pouvez lancer une nouvelle recherche de cette façon :

./torrengo Alexandre Dumas

Cela retournera tous les résultats trouvés sur tous les sites, le tout classé par nombre de seeders, et c’est à vous de décider lequel vous souhaitez alors télécharger :

Torrengo output

Vous pouvez aussi décider d’effectuer une recherche sur certains sites uniquement (disons The Pirate Bay et Archive.org) :

./torrengo -s tpb,arc Alexandre Dumas

Si vous souhaitez voir plus de logs (mode verbeux), ajoutez simplement le flag -v.

Rapide

Go étant un langage très performant et avec lequel il est facile de coder des programmes concurrents, Torrengo récupère la liste de torrents très rapidement. Au lieu de chercher sur chaque site un par un de façon séquentielle, il cherche sur tous en parallèle.

Torrengo est aussi rapide que la plus lente des réponse HTTP. Parfois la réponse HTTP la plus lente peut prendre plus de temps que vous ne l’auriez souhaité (par exemple Archive.org est assez lent depuis la France au moment où j’écris ces lignes). Dans ce cas, comme Torrengo supporte les timeouts, il vous suffit de déclarer un timeout comme suit :

./torrengo -t 2000 Alexandre Dumas

La commande ci-dessus stoppe toutes les requêtes HTTP qui prennent plus de 2 secondes et retourne les autres résultats trouvés.

Utilise les proxies pour The Pirate Bay

Les urls pour The Pirate Bay changent assez régulièrement et la plupart d’entre elles fonctionnent de façon erratique. Pour contourner ce problème, le programme lance une recherche concurrente sur toutes les urls The Pirate Bay trouvées sur un site listant tous les proxies (proxybay.bz à l’heure où j’écris ces lignes) et récupère les torrents depuis la réponse la plus rapide.

Le HTML de l’url retournée est aussi contrôlé en détails car certains proxies retournent parfois une page sans aucune erreur HTTP mais la page ne contient en réalité aucun résultat…

Contourner la protection Cloudflare sur Torrent Downloads

Télécharger des fichiers torrent depuis le site de Torrent Downloads est difficile car ce dernier a une protection Cloudflare qui consiste en un challenge Javascript auquel il faut répondre. Torrengo tente de contourner cette protection en résolvant ce challenge Javascript, chose qui fonctionne 90% du temps au moment à l’heure où j’écris cet article.

Authentification

Certains sites de torrents comme Ygg Torrent (anciennement t411) obligent les utilisateurs à s’authentifier afin de télécharger le fichier torrent. Torrengo gère cela en demandant ses identifiants à l’utilisateur (mais bien entendu il vous faut un compte en premier lieu).

Avertissement : ne faites jamais confiance aux programmes qui demandent vos identifiants sans avoir mis votre nez dans le code source au préalable.

Ouvrir dans le client torrent

Au lieu de copier coller les liens magnet trouvés par Torrengo dans votre client torrent ou de chercher le fichier torrent téléchargé pour l’ouvrir, vous pouvez simplement dire au programme d’ouvrir le torrent dans client local directement. Seul Deluge est supporté pour le moment.

Concurrence

Grâce à la simplicité de Go, la façon dont la concurrence est gérée dans ce programme n’est pas difficile à comprendre. C’est particulièrement vrai ici car la concurrence se prête particulièrement bien aux programmes faisant beaucoup d’appels réseau comme celui-ci. En gros la goroutine “main” crée une nouvelle goroutine pour chaque scraper et récupère les résultats dans des structs via des channels dédiés. tpb est un cas particulier puisque la bibliothèque crée à son tour n goroutines, n étant les nombre d’urls trouvées sur le site de proxies.

J’en profite pour dire que j’ai appris 2 petites choses intéressantes en termes de bonnes pratiques concernant les channels :

  • chaque goroutine de recherche retourne à la fois une erreur et un résultat. J’ai décidé de créer un channel dédié pour le résultat (une struct) et un autre pour l’erreur, mais je sais que certains préfèrent retourner les deux au sein d’une même struct. Ça dépend de vous, les deux sont ok.
  • les goroutines créées par tpb n’ont pas besoin de retourner une erreur mais simplement de signaler qu’elles se sont terminées. C’est ce que l’on appelle un “événement”. J’ai modélisé cela via l’envoi d’une struct vide (struct{}{}) sur la channel, ce qui semble le plus pertinent du point de vue de l’utilisation mémoire puisqu’une struct vide ne pèse rien. La seconde option et de passer un booléen ok, ce qui fonctionne parfaitement aussi.

Structure de la bibliothèque

Cette bibliothèque est composée de plusieurs sous bibliothèques : une pour chaque scraper. Le but est que chaque bibliothèque de scraping soit le moins couplée possible au programme général Torrengo afin de pouvoir être réutilisée dans des projets tiers. Chaque scraper a sa propre documentation.

Voici la structure actuelle du projet :

torrengo
├── arc
│   ├── download.go
│   ├── README.md
│   ├── search.go
├── core
│   ├── core.go
├── otts
│   ├── download.go
│   ├── README.md
│   ├── search.go
├── td
│   ├── download.go
│   ├── README.md
│   ├── search.go
├── tpb
│   ├── proxy.go
│   ├── README.md
│   ├── search.go
├── ygg
│   ├── download.go
│   ├── login.go
│   ├── README.md
│   ├── search.go
├── README.md
├── torrengo.go

Je vais vous donner quelques détails sur la façon dont j’ai organisé ce projet Go (j’aurais beaucoup aimé recevoir ce genre de conseils sur les bonnes pratiques à suivre concernant la structuration des projets Go).

Un package Go par répertoire

Comme je l’ai dit plus haut, j’ai créé une bibliothèque pour chaque scraper. Tous les fichiers contenus dans le même répertoire appartiennent au même package Go. Dans le dessin ci-dessus il y a 5 bibliothèques de scraping :

  • arc (Archive.org)
  • otts (1337x)
  • td (Torrent Downloads)
  • tpb (The Pirate Bay)
  • ygg (Ygg Torrent)

Chaque bibliothèque peut-être utilisée dans un autre projet go en l’important comme suit (par exemple pour Archive.org) :

import "github.com/juliensalinas/torrengo/arc"

La bibliothèque core contient les choses communes à toutes les bibliothèques de scraping, et sera ainsi importée par chacune d’elles.

Chaque bibliothèque de scraping a sa propre documentation (README) afin d’être parfaitement découplée.

Plusieurs fichiers par répertoire

Chaque répertoire contient plusieurs fichiers pour faciliter la lisibilité. Avec Go, vous pouvez créer autant de fichiers que vous le souhaitez pour le même package tant que vous mentionnez le nom du package en haut de chaque fichier. Par exemple ici chaque fichier contenu dans le dossier arc possède la ligne suivante tout en haut :

package arc

Par exemple dans arc j’ai organisé les choses de façon à ce que chaque struct, méthode, fonction, variable, liées au téléchargement du fichier torrent final aille dans le fichier download.go.

Commande

Certains développeurs Go mettent tout ce qui est lié aux commandes, c’est-à-dire les parties du programme dédiées à l’interaction avec l’utilisateur, dans un répertoire dédié cmd.

Ici, j’ai préféré mettre le tout dans un unique fichier torrengo.go à la racine du projet, ce qui me semblait plus approprié pour un petit programme comme celui-ci.

Conclusion

J’espère que vous aimerez Torrengo et j’adorerais recevoir des feed-back à ce sujet ! Les feed-backs peuvent concerner les fonctionnalités de Torrengo bien entendu, mais si vous êtes un développeur Go et que vous pensez que certaines parties du code peuvent être améliorées j’aimerais beaucoup entendre vos avis aussi.

Je prévois d’écrire d’autres articles autour de ce que j’ai appris en écrivant ce programme !

Also available in English
Bonnes lectures autour de Go, Python, et le management technique

Il arrive encore souvent que je dégote des informations précieuses dans les livres (informations que je n’aurais pas trouvées sur internet), et c’est pourquoi je lis beaucoup. J’ai eu l’occasion de lire pas mal de choses récemment et certaines ne valaient clairement pas le coup, mais d’autres m’ont beaucoup apporté, je les partage donc avec vous ici !

Go en long et en large

Si vous connaissez déjà les bases de Go (Golang) et que vous souhaitez approfondir, The Go Programming Language (Donovan & Kernighan) est un excellent ouvrage.

Personnellement, avant d’ouvrir ce livre j’avais déjà eu l’opportunité de développer plusieurs projets en Go mais certaines notions essentielles du langage me faisaient toujours défaut, notamment l’esprit du langage et la façon d’écrire du code idiomatique en Go. Ce livre m’a vraiment aidé. Il débute par les bases mais creuse le sujet très en profondeur jusqu’à aborder des points comme la réflexion, les tests, la programmation bas niveau… Les exemples sont très concis (je déteste lire trop de lignes de code dans un livre papier) et systématiquement adaptés de cas utiles de la vie réelle.

Go et la concurrence

Si vous estimez que l’ouvrage ci-dessus ne va pas assez loin en ce qui concerne la concurrence, vous pouvez lire Concurrency In Go (Cox-Buday).

Ce livre est très court et va droit au but. Il met vraiment l’accent sur les bonnes pratiques à appliquer lorsque l’on développe un programme concurrent (en général, et plus particulièrement en Go). Ce livre vous fournit des patterns que vous pouvez reproduire dans vos propres applications.

Astuces et outils autour de Python

Python est un langage relativement ancien, par conséquent de nombreux supers outils existent autour de ce langage (bibliothèques, frameworks, etc.). En revanche il n’est pas facile d’avoir une vision claire de toutes les options existantes. The Hitchhiker’s Guide to Python! (Reitz) résume tout ce que vous devez savoir à propos de l’écosystème Python. Il peut vous faire économiser énormément de temps. Attention : ce livre n’est pas un livre de code !

Le management technique

Si vous êtes en charge d’une équipe de développeurs vous savez déjà à quel point la tâche est complexe. Si vous n’êtes pas encore dans cette situation, alors soyez préparé ! Dans les deux cas il est important de se documenter sur le sujet. Manager et faire preuve de leadership est un job difficile, en particulier dans le domaine de la programmation (lead developer, manager technique, CTO, …). Ce livre a été d’une grande aide pour moi : Managing the Unmanageable (Mantle & Lichty). Il ne vous noie pas dans des concepts théoriques autour du management mais au contraire il vous donne de nombreux trucs et astuces faciles à appliquer concrètement.

Conclusion

Si vous avez lu l’un de ces ouvrages alors dîtes-moi ce que vous en avez pensé !

Also available in English
Python Flask vs Django

Mon expérience de Flask n’est pas aussi poussée que celle de Django, mais il se trouve que j’ai été récemment amené à développer plusieurs projets en Flask et il m’était impossible de ne pas comparer les deux. Je vais mener ici une comparaison très succincte, non pas en montrant du code mais plutôt en discutant de certains aspects plus “philosophiques”. Bien entendu mon opinion peut changer !

Les deux restent des frameworks Python…

Quand je suis passé de Django à Flask, ce qui m’a tout de suite frappé était la similitude entre les deux frameworks. Vous ne serez pas perdu. Cela tient principalement au fait que tous deux utilisent le langage Python. Bien que Flask soit censé être plus Pythonique (en utilisant beaucoup les décorateurs par exemple), je n’ai pas noté énormément de différences. Par ailleurs, les deux sont des frameworks web et la plupart des frameworks web fonctionnent à peu près de la même manière :

  • un ORM
  • du routage
  • un objet user request utilisé abondamment
  • des templates
  • la gestion des formulaires
  • la gestion des fichiers statiques

Même en passant de Python à un autre langage, l’utilisation d’un web framework vous aidera grandement parce que cette structure MVC sera toujours là (bon d’accord ce n’est pas toujours du vrai MVC mais peu importe) et vous vous sentirez comme chez vous en un instant. Par exemple, voyez comme c’est facile de passer de Python/Django à PHP/Laravel.

Framework vs micro framework

Flask est censé être un micro framework moins dirigiste et moins clé en main que Django. D’une certaine façon vous êtes censé être plus libre avec Flask. Et c’est le cas ! Django propose une structure de fichiers/dossiers que tout le monde respecte. Django contient aussi son propre ORM, son système de template, sa gestion des formulaires, son système d’inscription/authentification… qui sont utilisés par 99% des gens. Le problème est que, selon moi, cette liberté crée plus de problèmes qu’elle n’en résout…

Ce manque de structure implique que vous sachiez à l’avance exactement quelle sera la meilleure structure pour votre projet, ce qui est très difficile si vous n’avez jamais encore développé un projet entier avec Flask et que vous n’avez jamais eu l’occasion de mettre à l’épreuve cette structure sur un projet dont la taille grossit mois après mois. J’ai constaté que beaucoup de gens finissent par demander conseil concernant la structure de leur projet sur StackOverflow et enfin de compte il se trouve que la plupart des gens utilisent… la structure de Django (un fichier de configuration central et un dossier par application contenant chacun un views.py + models.py + forms.py + des templates) ! J’ai aussi eu pas mal de problèmes, comme beaucoup d’autres, d’imports circulaires en Python, ce qui n’arrive jamais avec Django puisque des gens intelligents ont conçu la structure à votre place justement pour vous éviter ce genre d’ennuis.

La structure d’un projet est une partie très importante de la documentation d’un projet. Une structure standardisée aidera considérablement les nouveaux venus sur votre projet. Chaque fois que je commence à travailler sur un projet Django existant, je suis complètement opérationnel en quelques minutes parce que je sais à l’avance comment les choses sont organisées. Ce n’est pas le cas avec Flask donc les développeurs d’un projet Flask doivent absolument écrire de la documentation supplémentaire afin d’expliquer la structure de leur projet. Le problème est le même avec de nombreuses bibliothèques natives sous Django mais pas sous Flask : chaque projet Flask peut être construit avec des bibliothèques différentes (pour les formulaires, les templates, l’inscription/login, etc.) ce qui fait que les nouveaux développeurs qui arrivent sur un projet peuvent avoir à se former sur de nouveaux outils avant de commencer à travailler.

Documentation

La documentation de Flask est bonne mais clairement trop difficile d’accès pour des développeurs débutants en comparaison de Django qui a une documentation s’adressant à tous types de niveaux (du pur débutant au plus avancé). Par ailleurs, Flask étant un micro framework, une grande partie du travail est basée sur l’utilisation de dépendances externes comme SQLAlchemy pour l’ORM, Flask login pour le login, Flask WTF pour les formulaires, etc. et certaines de ces dépendances n’ont pas le même standard de documentation que Flask. Dit autrement : je trouve la documentation de SQLAlchemy imbuvable, et les outils comme Flask login ont vraiment besoin de faire grossir leur doc (résoudre mes problèmes en parcourant le code source de la bibliothèque ne devrait jamais arriver selon moi).

Communauté

La communauté de Flask est bien plus petite que celle de Django (mais très accueillante malgré tout). La conséquence est que vous ne trouverez pas toutes vos réponses sur StackOverflow ou Github. Cependant vous finissez pas réaliser que la plupart des problèmes sont soit des problèmes purement Python (pas de souci dans ce cas) ou des problèmes rencontrés aussi par les utilisateurs de Django (dans ce cas on est aidé par la communauté de Django).

Conclusion

Flask est un micro framework qui ne recommande aucun standard ou convention. Selon moi il s’agit plus d’un problème que d’une solution si vous espérez que votre projet soit facilement maintenable par d’autre gens. En revanche, si la liberté vous attire, alors essayez Flask ! Si vous êtes déjà développeur Django, le changement sera très facile.

Also available in English