177 lines
10 KiB
Markdown
177 lines
10 KiB
Markdown
---
|
|
title: "Servir des chemins d'import Go personnalisés pour de nombreux modules avec nginx"
|
|
date: !!timestamp '2026-04-23 08:44:00'
|
|
slug: vanity-url-go-import-nginx
|
|
translationKey: vanity-url-go-import-nginx
|
|
tags:
|
|
- golang
|
|
- nginx
|
|
- hosting
|
|
---
|
|
|
|
Lorsque l'on publie du code Go, l'habitude est d'en distribuer les sources directement depuis la forge qui l'héberge : `github.com/org/projet`, `framagit.org/org/projet`, etc.
|
|
C'est pratique, mais cela lie durablement le chemin d'import au prestataire choisi.
|
|
Le jour où l'on migre d'une forge à une autre, tous les utilisateurs doivent modifier leurs imports, et chaque fork historique continue de pointer vers l'ancienne adresse.
|
|
|
|
Heureusement, le mécanisme [`go-import`](https://go.dev/ref/mod#vcs-find) permet de dissocier le chemin d'import du lieu d'hébergement réel du code.
|
|
Il suffit pour cela d'exposer, sur le domaine de son choix, une page HTML contenant une balise `<meta name="go-import">` décrivant où se trouvent les sources.
|
|
|
|
<!-- more -->
|
|
|
|
Cette technique est [décrite depuis longtemps sur le blog de Julien Vehent](https://jve.linuxwall.info/blog/index.php?post/2015/08/26/Hosting_Go_code_on_Github_with_custom_import_path), qui montre comment configurer HAProxy pour servir un fichier HTML statique renvoyant la balise adéquate pour un module unique.
|
|
Mais dès que l'on gère une poignée de modules, voire une collection entière, maintenir un fichier par module devient vite fastidieux.
|
|
Et l'approche historique ne répond pas non plus à la gestion des versions majeures du module, introduite [avec Go modules](https://go.dev/ref/mod#major-version-suffixes).
|
|
|
|
Ma solution a consisté à tout faire porter par `nginx`, au moyen d'une seule `location` utilisant une expression rationnelle.
|
|
|
|
|
|
## L'intérêt d'un chemin d'import personnalisé
|
|
|
|
Quand un utilisateur exécute `go get git.happydns.org/happydomain`, le client Go interroge d'abord l'URL `https://git.happydns.org/happydomain?go-get=1` et y cherche une balise :
|
|
|
|
```html
|
|
<meta name="go-import" content="git.happydns.org/happydomain git https://framagit.org/happyDomain/happydomain.git">
|
|
```
|
|
|
|
Cette balise indique à Go :
|
|
|
|
1. le préfixe qui sert d'identifiant pour ce module ;
|
|
2. le type de gestionnaire de versions (ici `git`) ;
|
|
3. l'URL réelle où cloner le dépôt.
|
|
|
|
Tant que l'on contrôle le domaine `git.happydns.org`, peu importe où se trouve réellement le code source.
|
|
On peut passer de GitHub à GitLab, de GitLab à Codeberg, ou même héberger sa propre forge : les chemins d'import restent identiques, et aucun utilisateur n'a besoin de modifier ses `import` ou son `go.mod`.
|
|
|
|
Ces plateformes pouvant changer leur conditions d'utilisation unilatéralement, il vaut toujours mieux être indépendant, et ne pas accroître la puissance de plateforme centralisées...
|
|
Il est donc important pour moi de garder la main sur l'identité des modules que je mets à disposition, sans avoir à gérer moi-même la forge qui les héberge.
|
|
|
|
|
|
## Limitations de l'approche par fichiers HTML
|
|
|
|
L'article initial propose de servir, pour chaque module, un fichier HTML statique contenant la bonne balise.
|
|
Cela fonctionne, mais présente deux inconvénients dès que l'on dépasse le module unique.
|
|
|
|
D'une part, chaque nouveau module ou renommage implique de créer ou mettre à jour un fichier supplémentaire sur le serveur web.
|
|
Pour le projet [happyDomain](https://www.happydomain.org/), j'héberge une trentaine de modules (le cœur, le SDK client, des bibliothèques de vérification, etc.) : autant de fichiers à synchroniser.
|
|
|
|
D'autre part, depuis l'arrivée des modules Go et de leur règle du *semantic import versioning*, tout module dont la version majeure dépasse 1 doit être importé avec un suffixe `/v2`, `/v3`, etc.
|
|
Le chemin `git.happydns.org/happydomain/v2` doit alors continuer à résoudre correctement, en renvoyant la même balise `go-import` que le module de base.
|
|
Servir un fichier HTML par module *et* par version majeure devient vite ingérable.
|
|
|
|
|
|
## Une `location` générique dans `nginx`
|
|
|
|
Plutôt que de multiplier les fichiers, je fais générer la réponse HTML directement par `nginx`, à partir d'une expression rationnelle qui capture à la fois le nom du module et son éventuel suffixe de version.
|
|
|
|
Voici la configuration en place sur `git.happydns.org` :
|
|
|
|
```
|
|
server {
|
|
# [...]
|
|
|
|
location / {
|
|
rewrite ^(.*)$ https://framagit.org/happyDomain$1;
|
|
}
|
|
|
|
location ~ ^/(golang-sdk|happydomain|happyDomain/happyDeliver|happydeliver|checker-sdk-go|checker-(caa|dav|delegation|dummy|email-autoconfig|kerberos|ldap|matrix|ns-restrictions|pgp|ping|smtp|ssh|sip|srv|stun-turn|tls|zonemaster))(/v[0-9])?$ {
|
|
default_type text/html;
|
|
return 200 '<!DOCTYPE html><html><head>
|
|
<meta name="go-import" content="git.happydns.org/$1$3 git https://framagit.org/happyDomain/$1.git">
|
|
<meta http-equiv="refresh" content="0; url=https://framagit.org/happyDomain/$1">
|
|
<link rel="me" href="https://floss.social/@happyDomain">
|
|
</head><body>Nothing to see here; <a href="https://framagit.org/happyDomain/$1">move
|
|
along</a>.</body></html>';
|
|
}
|
|
}
|
|
```
|
|
|
|
Décortiquons ce qui se passe.
|
|
|
|
|
|
## Dispatch des requêtes
|
|
|
|
La première instruction `location` renvoie toute requête qui ne correspond à rien d'autre vers l'URL équivalente sur Framagit.
|
|
Autrement dit, `git.happydns.org/happydomain/-/issues/42` renvoie l'utilisateur directement vers le bon ticket de la forge.
|
|
On conserve ainsi une cohérence totale entre l'adresse canonique et l'interface de la forge sous-jacente, sans avoir à dupliquer quoi que ce soit.
|
|
|
|
La dernière `location`, enfin, est celle qui nous intéresse : elle déclenche la génération de la réponse HTML pour les chemins d'import reconnus.
|
|
|
|
|
|
## L'expression rationnelle
|
|
|
|
Le motif `^/(golang-sdk|happydomain|happyDomain/happyDeliver|...)(/v[0-9])?$` reconnaît l'ensemble des modules que je distribue.
|
|
L'énumération explicite est volontaire : elle sert de liste blanche et évite que n'importe quel chemin inconnu déclenche la génération d'une balise `go-import` qui pointerait vers un dépôt inexistant.
|
|
|
|
Deux groupes de capture sont définis :
|
|
|
|
- `$1` correspond au nom du module ;
|
|
- `$3` correspond au suffixe de version (`/v2`, `/v3`, ...) lorsqu'il est présent, ou à une chaîne vide sinon. Le groupe `$2` est implicitement utilisé par les alternatives internes du second sous-motif `checker-(...)`.
|
|
|
|
On construit ensuite, dans la directive [`return 200`](https://nginx.org/en/docs/http/ngx_http_rewrite_module.html#return), une page HTML dont les éléments varient en fonction des captures.
|
|
|
|
|
|
## La réponse générée
|
|
|
|
La page renvoyée contient trois éléments utiles.
|
|
|
|
La balise `go-import` déclare au client Go où aller chercher les sources :
|
|
|
|
```html
|
|
<meta name="go-import" content="git.happydns.org/$1$3 git https://framagit.org/happyDomain/$1.git">
|
|
```
|
|
|
|
Notez que le chemin annoncé inclut le suffixe de version (`$3`) lorsqu'il existe, de sorte que `git.happydns.org/happydomain/v2` renvoie bien `git.happydns.org/happydomain/v2` comme identifiant de module.
|
|
L'URL du dépôt, elle, ne le comporte pas : Go se chargera de résoudre la bonne branche ou le bon tag à partir du contenu du dépôt.
|
|
|
|
Une directive [`meta refresh`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#http-equiv) redirige les navigateurs humains vers la page du projet sur Framagit.
|
|
Ainsi, lorsqu'un développeur colle dans son navigateur un chemin d'import qu'il a vu dans un `go.mod`, il arrive directement sur la page du dépôt, sans se retrouver face à une page blanche.
|
|
|
|
Enfin, un lien `rel="me"` associe le domaine au compte Mastodon du projet, pour la vérification d'identité sur le *fediverse*.
|
|
|
|
|
|
## Tester le résultat
|
|
|
|
On peut vérifier le bon fonctionnement de la configuration avec `curl`, en passant le paramètre `?go-get=1` que Go ajoute systématiquement :
|
|
|
|
```
|
|
42sh$ curl -s 'https://git.happydns.org/happydomain?go-get=1'
|
|
<!DOCTYPE html><html><head>
|
|
<meta name="go-import" content="git.happydns.org/happydomain git https://framagit.org/happyDomain/happydomain.git">
|
|
[...]
|
|
|
|
42sh$ curl -s 'https://git.happydns.org/happydomain/v2?go-get=1'
|
|
<!DOCTYPE html><html><head>
|
|
<meta name="go-import" content="git.happydns.org/happydomain/v2 git https://framagit.org/happyDomain/happydomain.git">
|
|
[...]
|
|
```
|
|
|
|
Pour s'assurer que le client Go accepte bien la réponse, l'option `-x` de `go get` (ou la variable `GOFLAGS="-x"`) affiche en détail toutes les requêtes effectuées lors de la résolution.
|
|
|
|
{{% card color="info" title="Une liste blanche, pas un catch-all" %}}
|
|
Il serait tentant de remplacer l'énumération explicite par un motif générique du type `^/([a-z0-9-]+)(/v[0-9])?$`.
|
|
Je préfère l'éviter : une faute de frappe dans un `import` produirait une réponse `200 OK` avec une balise `go-import` pointant vers un dépôt inexistant, et le message d'erreur renvoyé à l'utilisateur serait beaucoup moins explicite qu'un honnête `404`.
|
|
{{% /card %}}
|
|
|
|
{{% card color="danger" title="Attention à l'injection HTML" %}}
|
|
Soyez particulièrement vigilant sur la régularité de votre expression rationnelle : le contenu capturé est injecté tel quel dans le corps de la réponse HTML, sans aucun échappement de la part de `nginx`.
|
|
Un motif trop permissif comme `^/(.+)(/v[0-9])?$` ouvrirait la porte à des injections HTML, voire à du cross-site scripting (XSS) en glissant du code dans l'URL.
|
|
Restreignez toujours les caractères acceptés au strict nécessaire (`[a-zA-Z0-9_/-]` au plus), et préférez toujours une liste blanche lorsque c'est possible.
|
|
{{% /card %}}
|
|
|
|
|
|
## Ajouter un nouveau module
|
|
|
|
Lorsque j'ajoute un module au projet, la procédure se résume à deux modifications dans le fichier de configuration `nginx` :
|
|
|
|
1. insérer le nom du module dans l'expression rationnelle ;
|
|
2. recharger la configuration avec `nginx -s reload`.
|
|
|
|
Le module devient immédiatement importable sous `git.happydns.org/nouveau-module`, et la version majeure `/v2` fonctionne sans rien ajouter d'autre.
|
|
|
|
---
|
|
|
|
Cette technique est utilisée en production depuis plusieurs années sur [git.happydns.org](https://git.happydns.org/) sans que je ne m'en soucie jamais.
|
|
Elle me donne la garantie que, si je décide un jour de quitter Framagit pour une autre forge, il me suffira de modifier une seule URL dans ma configuration `nginx` pour que tous les utilisateurs actuels et passés de ces modules continuent d'accéder au code, sans avoir à modifier la moindre ligne chez eux.
|
|
|
|
L'identité d'un projet mérite d'être gérée indépendamment de l'outil qui l'héberge.
|
|
Quelques lignes d'`nginx` suffisent à se l'offrir.
|