HAProxy et en‑tête Host

Je suis tombé sur un problème avec le protocole HTTP qui ne m'étais jamais arrivé auparavant.

Le problème

Comme vous le savez, lorsqu'un navigateur souhaite obtenir une page d'un site internet, il fait ce qu'on appelle une requête HTTP. Cette requête contient entre autres un certain nombre d'en-têtes, dont un qui s'appelle Host.

Cet en-tête permet de signaler au serveur web quel est le site internet qui est interrogé, dans le cas où ce serveur web sert plusieurs sites à lui tout seul.

Par exemple, pour consulter Hashtagueule, votre navigateur emet une requête HTTPS sur le port 443 du serveur Hashtagueule avec un Host qui ressemble à ça :

Host: hashtagueule.fr

Le serveur regarde alors la liste des sites qu'il peut servir, il trouve le bon, et il renvoie le contenu demandé par la requête.

Ce que j'ignorais jusqu'alors, c'est que cet en-tête peut contenir de manière facultative le numéro de port distant. Pour Hashtagueule, cela donnerait :

Host: hashtagueule.fr:9091

RFC correspondante

Je ne comprends pas très bien pourquoi le numéro de port peut être inclus dans cet en-tête. D'autant plus que dans la pratique, les navigateurs incluent ce numéro de port dans certains cas seulement. D'après mes tests sous Firefox et curl (sans forcer l'en-tête avec l'option -H) :

  • Si le numéro de port correspond au port standard du protocole (80 pour HTTP, 443 pour HTTPS), alors ce numéro de port est omis de l'en-tête (et même si vous ajoutez ce numéro de port dans votre barre d'URL).
  • Si le numéro diffère du port standard, il est inclus dans l'en-tête.

Autrement dit, pour l'immense majorité des sites internet, ce numéro de port n'est jamais inclus. Ça explique comment on peut passer à côté de cette réalité.

Certains serveurs web comme nginx vous évitent de vous en soucier et font abstraction de ce numéro de port lorsqu'ils font la correspondence entre requête et site.

Par contre, d'autres comme HAProxy ne le font pas par défaut, et donc si vous n'êtes pas conscient de cela, vous risquez d'avoir des problèmes.

L'apprentissage par l'échec

J'utilise le programme Transmission sur mon serveur à la maison en mode "headless" (c'est à dire sur un serveur distant, avec une interface web). Celui-ci écoute d'habitude sur un port non standard, le port 9091. Cependant je l'ai placé derrière mon preverse proxy qui s'avère être HAProxy, ainsi tous mes sites HTTPS utilisent le port standard 443. Donc pour accéder à l'interface (et à l'API) de Transmission dans mon cas, il faut passer par le port standard 443.

tremotesf est une application Android qui se connecte à l'API de votre instance Transmission pour vous permettre de la contrôler, avec une interface sympa.

Dépôt Github de tremotesf

C'est en utilisant cette application que j'ai découvert le soucis : toutes les requêtes émises par cette application ont un en-tête Host contenant le numéro de port, qu'il soit standard ou non. Dans mon cas, donc, cela donnait ça :

Host: transmission.example.com:443

N'ayant pas prévu le coup, mon HAProxy était configuré pour mettre en correspondance Transmission avec le Host transmission.example.com, mais ne savait que faire avec le Host transmission.example.com:443.

La solution

Elle tient en une seule ligne, à placer dans la section Frontend :

http-request replace-header Host ([^:]+) \1

Cette directive dit simplement de supprimer les deux points et tout ce qui se trouve après, c'est à dire le numéro de port. On retombe donc bien sur un Host sans numéro de port.

Ainsi on ne m'y reprendra plus.