Blog of :/blog/weboob/Bug_SSL_dans_Python.html

Bug SSL dans Python

Depuis deux semaines, le site de la banque BNP Paribas se met parfois à s'arrêter brusquement de répondre à une requête HTTPS. Or ceci est bien fâcheux, car bien que weboob impose un timeout de 15 secondes à urllib2, il se trouve qu'en l'occurrence l'appel du module à la méthode urlopen() ne se termine jamais.

Armé de strace, j'ai pu constater que systématiquement il s'agit d'un appel à la fonction système read() qui n'obtient pas de réponse. Un peu plus d'investigation m'a révélé que cela se produit lors du handshake SSL.

Après avoir cherché en vain l'explication dans les sources du wrapper SSL de Python, j'ai trouvé un rapport de bug daté de 2007 et concernant la version 2.6. Il explique que le constructeur de la classe SSLSocket effectue le handshake directement, et ce avant qu'il ne soit possible d'interagir avec la socket pour la rendre non bloquante ou, ce qui m'intéresse davantage, de définir un timeout.

Le patch qui a été proposé et intégré introduit un paramètre do_handshake_on_connect, par défaut à True, ainsi que la méthode do_handshake(). Il s'agit d'une solution bas niveau, qui certes s'harmonise très bien à l'API de la lib SSL de Python qui ne brillait déjà pas par sa qualité.

Or le problème que j'ai avec weboob, est qu'on ne tape évidement pas directement dans ssl, mais qu'on passe par mechanize, qui utilise urllib2, qui utilise httplib, qui utilise ssl (ouf). Et aucune de ces bibliothèques ne supporte ce paramètre.

Malgré le fait que mechanize implémente tous les design patterns baveux existants, rendant son code complètement illisible, il ne semble pas possible de changer aisément le handler HTTPS, chose qui aurait pu permettre de surcharger la classe de httplib pour effectuer moi-même le handshake après avoir désactivé do_handshake_on_connect.

La solution crade que j'ai choisie m'a été inspirée d'un patch tout aussi crade envoyé dans un ticket pour contourner un bug de Debian Wheezy (toujours présent d'ailleurs) qui rend OpenSSL inopérant avec le site de la Banque Postale dès lors que l'on utilise le protocole par défaut (SSLv23).

Le code est le suivant, bien dissimulé au fond de weboob/tools/browser/browser.py :

import ssl

def mywrap_socket(sock, *args, **kwargs):
    kwargs['do_handshake_on_connect'] = False
    kwargs['ssl_version'] = kwargs.get('ssl_version', ssl.PROTOCOL_TLSv1)
    sock = ssl.wrap_socketold(sock, *args, **kwargs)
    sock.settimeout(StandardBrowser.DEFAULT_TIMEOUT)
    # check if we are already connected
    try:
        sock.getpeername()
    except:
        sock.do_handshake_on_connect = True
    else:
        sock.do_handshake()
    return sock

ssl.wrap_socketold=ssl.wrap_socket
ssl.wrap_socket=mywrap_socket

httplib utilise la fonction wrap_socket pour créer la socket SSL. En remplaçant cette méthode, en changeant les arguments et en effectuant le handshake nous-même dans le cas où nous sommes déjà connectés, ça peut enfin fonctionner.

Et dire que nous sommes en 2012.