Blog of :/blog/

Categories:
  • weboob
  • BudgetInsight
  • weboob / 5 ans de weboob

    weboob / Synchroniser Budgea avec weboob

    Un reproche qui est fait régulièrement à Budgea est qu'il est nécessaire d'y enregistrer ses identifiants bancaires pour utiliser le service.

    En effet, pour des raisons évidentes de convivialité, nous préférons stocker (de manière sécurisée¹) les credentials et effectuer la synchronisation quotidiennement (via weboob) pour générer des alertes, et éviter que l'utilisateur ait un agent sur son poste qui doit être régulièrement mis à jour.

    Néanmoins, grâce à l'API fournie par Budgea, il est possible de charger les nouvelles transactions depuis une application cliente. J'ai donc rajouté une commande à l'application boobank pour effectuer la synchronisation depuis sa propre machine.

    Pour cela, une fois votre compte créé sur Budgea, vous pouvez charger vos comptes et transactions bancaires en une seule commande :

    $ boobank budgea USERNAME PASSWORD
    

    Par exemple :

    $ boobank budgea romain@weboob.org mypassword
    - C/C Eurocompte Confort M Machin (678.00€): 27 new transactions
    - Compte Courant M Machin (0.00€): 9 new transactions
    

    Les comptes apparaissent maintenant dans Budgea avec l'ensemble des transactions disponibles. Il est dès lors possible d'ajouter cette commande dans un cron pour que cela soit fait régulièrement.

    ¹ le chiffrement est réalisée de manière asymétrique, ce qui fait que seuls les backends, non accessibles depuis Internet, peuvent lire les identifiants bancaires pour les envoyer aux sites des banques.

    weboob / Browser2 : La pagination

    Cet article fait partie d'une série sur le Browser2.

    Beaucoup de méthodes de modules weboob retournent des listes d'éléments itérés sur des pages paginées. C'est le cas des résultats de recherches notamment.

    Voici l'exemple typique de code avec l'ancien browser permettant de gérer cette pagination :

    class ResultsPage(BasePage):
        def iter_results(self):
            # ...
    
        def get_next_url(self):
            link = self.document.xpath('//a[text()="Next »"]')
    
            if not link:
                return None
    
            return link[0].attrib["href"]
    
    class Browser(BaseBrowser):
        # ...
        def search_pattern(self, pattern):
            self.location('/search/%s' % urllib.quote(pattern))
    
            while True:
                assert self.is_on_page(ResultsPage)
    
                for video in self.page.iter_results():
                    yield video
    
                next_url = self.page.get_next_url()
                if next_url is None:
                    return
    
                self.location(next_url)
    

    Plus verbeux, on ne fait pas.

    Le Browser2 introduit un mécanisme qui permet de gérer beaucoup plus simplement la pagination, dont le fonctionnement interne est simple :

    • La méthode de la page, une fois qu'elle a fini d'envoyer les éléments de la page, doit envoyer une exception NextPage avec le lien ou la requête à exécuter ;
    • Un mécanisme au dessus de la page se charge de capturer cette exception, joue la requête, et appelle de nouveau la méthode sur la nouvelle page.

    Bien camouflé dans ListElement et le décorateur pagination, cela donne :

    class ResultsPage(HTMLPage):
        @pagination
        @method
        class iter_results(ListElement):
            item_xpath = '//span[@id="miniatura"]'
    
            next_page = Link(u'//a[text()="Next »"]')
    
            class item(ItemElement):
                # ...
    
    class Browser(PagesBrowser):
        # ...
        def search_pattern(self, pattern):
            self.search.go(pattern=pattern)
            assert self.search.is_here(pattern=pattern)
    
            return self.page.iter_results()
    

    Définir l'attribut next_page fait que si le lien est présent, ListElement lance l'exception NextPage. Ensuite, le décorateur pagination se charge de la traiter lorsqu'elle est capturée.

    Note : une autre façon de gérer la pagination existe avec la méthode PagesBrowser.pagination().

    Anonyrcd

    TL;DR : /connect parano.me
    

    Récemment, lors d'une beuverie hebdomadaire, mon pote juke m'a parlé d'une idée intéressante, à savoir d'avoir un channel IRC où personne ne saurait qui écrit quoi.

    L'intérêt est multiple :

    • Supprimer le biais de réputation qui fait qu'on accorde davantage de crédit aux propos de leto que de clemux ;
    • Rendre le suivi des conversations confusant ;
    • Tenter de reconnaître à partir du style l'auteur de tel ou tel message.

    Connaissant les tendances de juke à la procrastination, je me suis mis en tête de coder ça rapidement. Je me suis donc basé sur miniircd, un petit serveur IRC de 500 lignes écrit en Python, que j'ai adapté pour introduire les notions suivantes :

    Timestamp channels

    Sur les chans dont le nom est préfixé par #, les pseudos sont les timestamps. Le principe est un peu le même que la tribune DLFP à son origine.

    <16:53:42> ça je sais qui c'est !
    <16:53:47> ^ça aussi
    <16:53:52> :D
    <16:53:56> oui mais c'est parce que tu m'entends taper au clavier batard !
    <16:53:57> hahaha
    <16:54:17> 16:53:42: en es-tu sûr ?
    <16:54:27> 16:54:17 oh oui
    

    Retirement channels

    Sur les chans dont le nom est préfixé par +, des prénoms choisis aléatoirement sont utilisés. Évidemment, il n'y a pas d'association fixe, ce qui rajoute à la confusion générale.

    <Patoche> Hector: je m'étais emmerdé à regarder dans le code et j'avais pas trouvé
    <Jocelyne> faut dire aussi que irker c'est pas le truc le plus simple à debugger
    <Gilbert> faut utiliser la version 2.x déjà
    <Gilbert> Gilbert: symlink.me est en squeeze, donc bon…
    <Liliane> il est simple le code du serveur irc, ça peut etre pratique pouir faire une ihm
    <Gilbert> une ihm ?
    <Hector> quand on veut pas laisser un acces ssh pour lancer certaines commandes par exemple
    <Liliane> c'est quoi cette idée de merde ?
    

    The finding game

    elfangor a contribué un patch qui introduit une commande /KICK qui permet d'essayer de deviner qui est l'auteur de tel message.

    Il suffit de faire /kick 16:54:17 pankkake, en cas de victoire le message suivant est posté sur le chan 

    <irc.parano.me> romain has found pankkake
    

    Rejoindre la folie

    /connect parano.me
    

    Sources

    $ git clone git://git.symlink.me/pub/romain/anonyrcd.git
    

    Payer son patch

    $ git format-patch -n -s origin
    $ git send-email --to=romain@symlink.me *.patch
    

    weboob / Browser2 : La classe TableElement

    Cet article fait partie d'une série sur le Browser2.

    On a vu la classe ListElement qui itère sur les éléments d'une liste du HTML pour en sortir des objets weboob, je vais maintenant introduire la classe TableElement dont l'objet est de simplifier le traitement des tableaux

    Prenons le code suivant :

    class HistoryPage(HTMLPage):
        @method
        class get_history(ListElement):
            item_xpath = '//table[@class="liste"]/tbody/tr'
    
            class item(ItemElement):
                klass = Transaction
                condition = lambda self: len(self.el.xpath('./td')) >= 3
    
                obj_date = Transaction.Date('./td[1]')
                obj_raw = Transaction.Raw('./td[2]')
                obj_amount = Transaction.Amount('./td[last()]')
    

    Grâce aux filtres, c'est parfaitement lisible, néanmoins ça souffre d'un problème. Si, comme le site du Crédit Mutuel, l'ordre des colonnes change ou des colonnes supplémentaires sont présentes (en fonction des clients), il est difficile de savoir où se trouve l'information.

    Pour ce cas, la classe TableElement a été rajoutée afin de faire la sélection de la cellule non pas à partir de son index, mais à partir de son titre.

    class HistoryPage(HTMLPage):
        @method
        class get_history(TableElement):
            head_xpath = '//table[@class="liste"]//thead//tr/th/text()'
            item_xpath = '//table[@class="liste"]//tbody/tr'
    
            col_date = u"Date de l'annonce"
            col_raw = u"Opération"
            col_amount = u"Montant"
    
            class item(ItemElement):
                klass = Transaction
                condition = lambda self: len(self.el.xpath('./td')) >= 3
    
                obj_date = Transaction.Date(TableCell('date'))
                obj_raw = Transaction.Raw(TableCell('raw'))
                obj_amount = Transaction.Amount(TableCell('amount'))
    

    Comme on peut le voir, il suffit pour cela de fournir le xpath vers les différents titres de colonnes, ainsi que les titres associés à des identifiants. Ceux-ci peuvent alors être réutilisés dans ItemElement pour sélectionner cette cellule et la traiter dans les filtres suivants.

    weboob / Browser2 : Parsing descriptif du contenu des pages

    Cet article fait partie d'une série sur le Browser2.

    Le parsing des pages est un élément important de weboob car il s'agit du cœur de la problématique à laquelle répond le logiciel, mais aussi la partie la plus sensible et la plus complexe car c'est ici que l'on a affaire aux webmasters incompétents.

    Ainsi qu'on l'a vu dans un un post précédent, à chaque url est associée une classe dérivée de BasePage qui est instanciée et qui traite la page pour en sortir des données structurées.

    L'ancien régime

    Voici à quoi ressemble l'implémentation d'une BasePage avec le browser actuel :

    class IndexPage(BasePage):
        def iter_videos(self):
            span_list = self.parser.select(self.document.getroot(), 'span#miniatura')
            for span in span_list:
                a = self.parser.select(span, 'a', 1)
                url = a.attrib['href']
                _id = re.sub(r'/videos/(.+)\.html', r'\1', url)
    
                video = YoujizzVideo(_id)
    
                video.thumbnail = BaseImage(span.find('.//img').attrib['data-original'])
                video.thumbnail.url = video.thumbnail.id
    
                title_el = self.parser.select(span, 'span#title1', 1)
                video.title = to_unicode(title_el.text.strip())
    
                time_span = self.parser.select(span, 'span.thumbtime span', 1)
                time_txt = time_span.text.strip().replace(';', ':')
                hours, minutes, seconds = 0, 0, 0
                if ':' in time_txt:
                    t = time_txt.split(':')
                    t.reverse()
                    seconds = int(t[0])
                    minutes = int(t[1])
                    if len(t) == 3:
                        hours = int(t[2])
                elif time_txt != 'N/A':
                    raise BrokenPageError('Unable to parse the video duration: %s' % time_txt)
    
                video.duration = datetime.timedelta(hours=hours, minutes=minutes, seconds=seconds)
    
                yield video
    

    La révolution

    class IndexPage(HTMLPage):
        @method
        class iter_videos(ListElement):
            item_xpath = '//span[@id="miniatura"]'
    
            next_page = Link(u'//a[text()="Next »"]')
    
            class item(ItemElement):
                klass = BaseVideo
    
                obj_id = Regexp(Link('.//a'), r'/videos/(.+)\.html')
                obj_title = CleanText('.//span[@id="title1"]')
                obj_duration = Duration(CleanText('.//span[@class="thumbtime"]//span'), default=NotAvailable)
                obj_nsfw = True
    
                def obj_thumbnail(self):
                    thumbnail = BaseImage(Attr('.//img', 'data-original')(self))
                    thumbnail.url = thumbnail.id
                    return thumbnail
    

    On le voit facilement, outre la diminution du nombre de lignes, c'est vraiment beaucoup plus lisible. Ceci s'explique par le fait que si la première implémentation est très procédurale, le nouveau style introduit et permis par le browser2 se veut descriptif.

    Certes, il y a beaucoup de magie. Je me propose d'expliquer et de détailler chaque partie afin d'en comprendre les concepts.

    HTMLPage

    class IndexPage(HTMLPage):
    

    La classe dont hérite IndexPage est HTMLPage. En effet, maintenant ce n'est plus au niveau du browser que s'effectue la désérialisation du document, mais au niveau de la page elle-même, ce qui est plus logique. On aura donc également JSonPage, CSVPage, etc.

    ListElement

    @method
    class iter_videos(ListElement):
    

    Une classe majeure qui a été introduite est ListElement. Elle est censée permettre, grâce à ses attributs, d'automatiser la découverte et l'itération de membres d'une liste dans la page. Le principe est que coder doit rester l'exception, parser les pages doit au maximum être du paramétrage.

    Puisqu'on appelle iter_videos comme une méthode à partir du browser, le décorateur @method a été rajouté afin que ce soit fait de façon transparente et analogue à la première implémentation.

    L'attribut obligatoire pour ListElement est item_xpath :

    item_xpath = '//span[@id="miniatura"]'
    

    On y précise la chaîne xpath qui sera utilisée pour itérer sur les éléments.

    next_page = Link(u'//a[text()="Next »"]')
    

    Cet attribut est utile pour traiter la pagination. On verra dans un prochain article comment cela fonctionne.

    ItemElement

    class item(ItemElement):
    

    Il s'agit de la classe assurant le parsing d'un item de la liste, en instanciant un objet hérité de CapBaseObject et remplissant ses champs.

    klass = BaseVideo
    

    Le concept est le suivant : on définit quelle classe on utilise, et pour chaque champ, on crée un attribut obj_<NAME> qui peut être soit un filtre, soit une constante, soit une méthode retournant la valeur.

    Un filtre est un objet dont on appelle la méthode __call__ sur l'item et qui a la particularité de pouvoir être chainé avec un ou plusieurs autres filtres.

    Le browser2 propose un certain nombre de filtres par défaut, qui sont utilisés dans notre exemple. Le développeur du module peut également définir ses propres filtres.

    obj_id = Regexp(Link('.//a'), r'/videos/(.+)\.html')
    

    On utilise le filtre Link pour récupérer le lien de la balise dont on a fournit le xpath, puis on chaîne avec le filtre Regexp pour en extraire l'identifiant de la vidéo qui est contenu dans l'url.

    obj_title = CleanText('.//span[@id="title1"]')
    

    Le filtre CleanText est utilisé pour récupérer tout le texte contenu dans l'élément sélectionné et ses fils, et le nettoyer (suppression des multiples espaces, des tabulations, etc.)

    obj_duration = Duration(CleanText('.//span[@class="thumbtime"]//span'), default=NotAvailable)
    

    On utilise le filtre CleanText pour retourner le texte purgé de l'élément sélectionné, puis le filtre Duration le parse et renvoie un objet datetime.timedelta.

    L'argument default précise que si la durée n'a pas pu être récupérée, la valeur NotAvailabe sera prise. Sans cet argument, une exception est lancée.

    obj_nsfw = True
    

    Le champ nsfw se verra tout le temps attribuer la constante True.

    def obj_thumbnail(self):
        thumbnail = BaseImage(Attr('.//img', 'data-original')(self))
        thumbnail.url = thumbnail.id
        return thumbnail
    

    Ici, obj_thumbnail est une méthode qui est appelée et qui retourne la valeur que l'on souhaite mettre dans le champ thumbnail.

    À noter l'utilisation du filtre Attr qui sélectionne une image pour en extraire la valeur de l'attrib data-original.

    Liens vers la documentation

    weboob / Browser2 : Les formulaires

    Cet article fait partie d'une série sur le Browser2.

    Mechanize offre la possibilité de remplir et de soumettre le formulaire d'une page assez facilement, mais souffre de nombreux problèmes. Prenons par exemple ce code issu du module ING :

    def login(self, password):
        # ...
        self.browser.select_form('mrc')
        self.browser.set_all_readonly(False)
        self.browser.controls.append(ClientForm.TextControl('text', 'mrc:mrg', {'value': ''}))
        self.browser.controls.append(ClientForm.TextControl('text', 'AJAXREQUEST', {'value': ''}))
        self.browser['AJAXREQUEST'] = '_viewRoot'
        self.browser['mrc:mrldisplayLogin'] = vk.get_string_code(realpasswd)
        self.browser['mrc:mrg'] = 'mrc:mrg'
        self.browser['sens'] = ['1']
        self.browser.submit()
    

    On peut noter les choses suivantes :

    • La sélection se fait par le browser (à partir du nom du formulaire)
    • Ce système est stateful, c'est à dire qu'on sélectionne un formulaire, puis on change des attributs du browser pour définir les valeurs
    • Par défaut, mechanize respecte les contraintes de la page, empêchant par exemple de modifier les champs désactivés ou les <input type="hidden">. Il est nécessaire d'appeler set_all_readyonly(False) pour contourner ça
    • Le parsing des pages est mal foutu, ce qui fait qu'il loupe parfois des champs. On est alors obligé de les rajouter nous-mêmes
    • Lorsque le champ est à valeurs multiples (un <select> par exemple), on doit passer une liste plutôt qu'une chaîne, et il gueule si il n'y a pas de champ <option> ayant cette valeur

    Enfin, si le formulaire n'a pas de nom, il n'est possible que de passer un prédicat de ce genre :

    self.browser.select_form(predicate=lambda x: x.attrs.get('id','')=='setInfosCGS')
    

    Browser2 à la rescousse

    L'approche du nouveau système est un peu différente. On décorrèle les formulaires du browser en rajoutant une méthode get_form() à la page, qui retourne un objet Form sur lequel on peut modifier et rajouter des champs sans restriction. On réécrirait le code ci-dessus comme suit :

    def login(self, password):
        # ...
        form = self.get_form(name='mrc')
        form['AJAXREQUEST'] = '_viewRoot'
        form['mrc:mrldisplayLogin'] = vk.get_string_code(realpasswd)
        form['mrc:mrg'] = 'mrc:mrg'
        form['sens'] = '1'
        form.submit()
    

    La méthode get_form accepte également un paramètre xpath qui, comme son nom l'indique, est une chaîne xpath :

    form = self.get_form(xpath='//form[@id="setInfosCGS"]')
    

    Liens vers la documentation

    weboob / Browser2 : Nouvelle gestion des pages avec la classe URL

    Cet article fait partie d'une série sur le Browser2.

    Le browser est séparé en deux niveaux : les pages, qui sont représentées chacune par des classes dérivées de BasePage contenant le code nécessaire au parsing du contenu des pages, et le Browser lui-même qui sert de contrôleur pour la navigation.

    L'ancien système

    Voici ce à quoi ressemble le système de pages de la première version du browser :

    class CreditMutuelBrowser(BaseBrowser):
        PROTOCOL = 'https'
        DOMAIN = 'www.creditmutuel.fr'
        PAGES = {'https://www.creditmutuel.fr/.*/fr/banque/situation_financiere.cgi': AccountsPage,
                 'https://www.creditmutuel.fr/.*/fr/banque/mouvements.cgi.*': OperationsPage,
                 'https://www.creditmutuel.fr/.*/fr/banque/nr/nr_devbooster.aspx.*': OperationsPage,
                }
    
        def get_accounts_list(self):
            if not self.is_on_page(AccountsPage):
                self.location('https://www.creditmutuel.fr/%s/fr/banque/situation_financiere.cgi' % self.currentSubBank)
            return self.page.get_list()
    

    Un dictionnaire fait la correspondance entre une expression régulière d'url et la classe d'une page (dérivée de BasePage), et lorsque l'on fait un appel à la méthode location() il tente de retrouver la page à instancier en fonction de l'url. On peut donc ainsi savoir sur quelle page on se trouve avec la méthode is_on_page(klass), qui est un alias à isinstance(self.page, klass).

    La classe URL

    Voici maintenant comment on décrit les pages dans le browser2 :

    class CreditMutuelBrowser(PagesBrowser):
        BASEURL = 'https://www.creditmutuel.fr'
    
        accounts =   URL('/(?P<subbank>.*)/fr/banque/situation_financiere.cgi',
                         AccountsPage)
        operations = URL('/(?P<subbank>.*)/fr/banque/mouvements.cgi.*',
                         '/(?P<subbank>.*)/fr/banque/nr/nr_devbooster.aspx.*',
                         OperationsPage)
    

    Plutôt que d'avoir un dictionnaire, on décrit des attributs à la classe avec associé d'une part une liste de chemins, et d'autre part la classe à instancier. L'attribut BASEURL permet de préciser le chemin plutôt que des urls complètes.

    En outre, la résolution se fait maintenant dans l'ordre de déclaration, ce qui est très utile dans le cas de conflits.

    Reverse

    Vous avez sans doutes constaté le fait que dans nos expressions régulières ont été précisés des noms pour le pattern subbank. Ceci permet d'une part de récupérer les valeurs dans l'instance de BasePage, mais surtout d'utiliser URL comme un moyen d'accéder directement aux pages avec des paramètres variables.

    En effet, URL fournit deux méthodes très sympathiques, URL.go() et URL.stay_or_go(), ce qui nous permet de réécrire la méthode get_accounts_list comme ceci :

        def get_accounts_list(self):
            return self.accounts.stay_or_go(subbank=self.currentSubBank).get_accounts()
    

    L'avantage majeur de ce système est donc d'éviter, comme dans l'exemple du browser1, d'avoir une redondance entre les URLs à associer aux BasePage et les appels à location() où l'on doit à nouveau spécifier l'url.

    Liens vers la documentation

    weboob / Browser2 : Introduction

    Weboob existe depuis plus de quatre ans, et est issu de deux projets, aum (2008) et bnporc (2009). Une composante assez importante est le Browser, une classe accompagnée d'un ensemble d'outils simulant le comportement d'un navigateur et aidant au scrapping des sites web.

    Toujours présente aujourd'hui dans weboob, elle tire ses racines d'aum et n'a fait qu'évoluer petit à petit, en gardant la retro-compatibilité de l'API. Autant dire que depuis ses six années d'existence, c'est une usine à gaz sans cohérence qui s'est construite.

    Un autre soucis majeur du Browser est qu'il dépend de mechanize, une bibliothèque Python rajoutant une surcouche à urllib2, et qui multiplie les inconvénients (API catastrophique, support limité du SSL, quasiment plus maintenu, etc.).

    Mais fort de l'expérience en scrapping qui a été acquise par l'équipe depuis toutes ces années, et grâce à l'apparition de python-requests, une bibliothèque capable de remplacer avantageusement mechanize, nous avons décidé de procéder à l'écriture d'un Browser2.

    Le projet a été démarré en novembre 2012 par Laurent Bachelier, mais à cette époque requests était encore assez peu mature et l'API évoluait sans cesses. Cela a conduit à mettre de côté les développements.

    Mais la semaine dernière, pris d'un sursaut de motivation, j'ai repris en main le projet afin de supporter la version 2.0 de requests et de dessiner les nouveaux concepts du Browser2 pour aboutir à une première implémentation qui est maintenant mergée dans le dépôt de développement. Plusieurs modules (Crédit Mutuel, Youjizz et Hybride) ont déjà été portés.

    Même si l'API est amenée à évoluer, je me propose de rédiger quelques articles durant les prochaines semaines se focalisant sur les nouveaux concepts introduits par le Browser2, notamment :

    VPN d'entreprise avec Debian + OpenVPN + BIND

    Introduction

    L'idée est de créer un LAN virtuel pour les salariés, avec un sous-domaine dédié, et des services qui écoutent uniquement sur cette interface.

    Pour ce faire, on se repose sur les briques suivantes :

    • Debian Jessie
    • OpenVPN 2.3
    • BIND 9.8

    Nous supposerons que le nom de domaine de l'entreprise est example.com, et que le sous-domaine associé aux services internes est lan.example.com.
    Le sous-réseau dédié au VPN sera quant à lui 10.22.33.0/24.

    Nous allons également faire rediriger tout le trafic de nos utilisateurs via le VPN, afin de sécuriser le réseau local où l'on peut facilement intercepter les paquets.

    OpenVPN

    Commençons par l'installation d'OpenVPN :

    # apt-get install openvpn easy-rsa
    

    PKI

    Pour créer la PKI contenant les certificats qui seront dédiés au serveur VPN, on utilise la commande suivante :

    # make-cadir /etc/openvpn/ssl
    

    On peut se déplacer dans ce répertoire, et éditer les paramètres KEY_* du fichier vars pour configurer les valeurs par défaut des certificats :

    # cd /etc/openvpn/ssl
    # vi vars
    [...]
    export KEY_COUNTRY="FR"
    export KEY_PROVINCE="Ile-de-France"
    export KEY_CITY="Paris"
    export KEY_ORG="Example Organization"
    export KEY_EMAIL="admin@example.com"
    export KEY_OU="Example LAN"
    [...]
    

    On peut maintenant procéder à l'initialisation de la PKI, à la création de la CA, ainsi que du certificat du serveur :

    # source ./vars
    # ./clean-all
    # ./build-dh
    # ./build-ca
    [...]
    # ./build-key-server vpn.example.com
    [...]
    

    Lors du prompt des créations de clefs, vous pouvez laisser les valeurs par défaut. Il n'est pas nécessaire de mettre de Challenge Password.

    Pour chaque membre du VPN, il sera nécessaire de créer un certificat de cette manière :

    # ./build-key employee_name.lan.example.com
    

    Vous pouvez également demander à vos utilisateurs d'envoyer un CSR que vous pouvez vous contenter de signer.

    Une fois les certificats générés, ils se retrouvent dans keys/. Il vous faudra transmettre ca.crt ainsi que la clef et le certificat généré à chacun de vos utilisateurs.

    Configuration serveur

    Créez le fichier /etc/openvpn/server.conf pour y mettre les lignes suivantes :

    local 0.0.0.0
    proto tcp
    dev tun
    
    ca /etc/openvpn/ssl/keys/ca.crt
    cert /etc/openvpn/ssl/keys/vpn.example.com.crt
    key /etc/openvpn/ssl/keys/vpn.example.com.key
    dh /etc/openvpn/ssl/keys/dh1024.pem
    
    server 10.22.33.0 255.255.255.0
    ifconfig-pool-persist ipp.txt
    
    keepalive 10 60
    
    persist-key
    persist-remote-ip
    persist-local-ip
    comp-lzo
    
    # Autorise la communication entre les clients
    client-to-client
    
    # Indique aux clients d'utiliser ce serveur
    # DNS par défaut
    push "dhcp-option DNS 10.22.33.1"
    push "dhcp-option DOMAIN lan.budget-insight.com"
    

    Activez ip_forward pour rediriger le trafic :

    # echo "net.ipv4.ip_forward=1" >>/etc/sysctl.conf
    # sysctl net.ipv4.ip_forward=1
    

    Comme vos utilisateurs vont utiliser le VPN comme route par défaut pour sortir sur Internet, vous devez créer les règles iptables suivantes :

    # iptables -t nat -I POSTROUTING -s 10.22.33.0/24 -j MASQUERADE
    # iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
    

    Vous pouvez maintenant lancer le serveur VPN :

    # /etc/init.d/openvpn start
    

    Configuration client

    Le client doit installer les paquets suivants :

    # apt-get install openvpn resolvconf
    

    Une fois que vos utilisateurs ont mis la CA, la clef et le certificat dans /etc/openvpn/ssl/, ils doivent écrire le fichier /etc/openvpn/client.conf suivant :

    client
    
    dev tun
    proto tcp
    
    remote vpn.example.com 1194
    
    resolv-retry infinite
    
    nobind
    
    persist-key
    persist-tun
    
    ca certs/ca.crt
    cert certs/employee_name.lan.example.com.crt
    key certs/employee_name.lan.example.com.key
    
    ns-cert-type server
    comp-lzo
    verb 3
    
    # Utilise le VPN comme route par défaut
    redirect-gateway def1
    
    # Active la redéfinition du serveur DNS à utiliser
    script-security 3 execve
    up /etc/openvpn/update-resolv-conf
    down /etc/openvpn/update-resolv-conf
    

    Il ne reste plus qu'à lancer le VPN :

    # /etc/init.d/openvpn start
    

    BIND

    Maintenant le VPN en place, on veut pouvoir configurer BIND pour être à la fois un serveur DNS interne au réseau, un relai pour les utilisateurs, et un serveur d'autorité pour le nom de domaine sur Internet.

    Éditons les fichiers de configuration de /etc/bind/ :

    named.conf.options

    options {
        directory "/var/cache/bind";
    
        forwarders {
            // Utilisation des serveurs DNS de Google pour
            // relayer les requêtes DNS faites depuis le LAN
            8.8.8.8;
            8.8.4.4;
        };
    
        auth-nxdomain no;    // conform to RFC1035
        listen-on { any; };
        listen-on-v6 { any; };
        allow-recursion {
            10.22.33.0/24;
            localhost;
            ::1;
            127.0.0.1;
        };
    
        // Serveur DNS secondaire du domaine example.com
        allow-transfer { 213.186.33.199; };
    };
    

    named.conf.local

    view "internal" {
        match-clients {
            10.22.33.0/24;
            127.0.0.1;
        };
        recursion yes;
    
        zone "example.com" {
                type master;
                file "/etc/bind/zone/example.com.lan";
                allow-transfer { any; };
        };
    };
    
    view "external" {
        match-clients { any; };
        recursion no;
    
        include "/etc/bind/named.conf.default-zones";
    
        zone "example.com" {
                type master;
                file "/etc/bind/zone/example.com";
                notify yes;
                // Serveur DNS secondaire
                notify-source 213.186.33.199;
        };
    };
    

    Le système de vues de BIND permet de définir des typologies de clients. On a d'une part les clients internes qui, pour le domaine example.com, vont avoir une zone dédiée, et pour qui les requêtes sur les autres domaines vont être forwardées, et les clients externes qui ne peuvent requêter que sur les domaines de l'entreprise.

    zone/example.com

    $TTL 12H
    @       IN   SOA   example.com.  admin.example.com. (
                               2014022101
                               8H
                               30M
                               4W
                               8H )
    
            IN   NS    ns.example.com.
                 NS    ns.kimsufi.com.
    
            IN   MX    10 mail.example.com.
            IN   MX    20 mail.symlink.me.
    
    @       IN   A     88.11.22.33
    mail    IN   A     88.11.22.33
    ns      IN   A     88.11.22.33
    www     IN   A     88.11.22.33
    vpn     IN   A     88.11.22.33
    
    $ORIGIN example.com.
    

    zone/example.com.lan

    $TTL 12H
    @       IN   SOA   example.com.  admin.example.com. (
                               2014022101
                               8H
                               30M
                               4W
                               8H )
    
            IN   NS    ns.example.com.
                 NS    ns.kimsufi.com.
    
            IN   MX    10 mail.example.com.
            IN   MX    20 mail.symlink.me.
    
    @       IN   A     10.22.33.1
    mail    IN   A     10.22.33.1
    ns      IN   A     10.22.33.1
    www     IN   A     10.22.33.1
    vpn     IN   A     10.22.33.1
    
    git.lan         IN   A   10.22.33.1
    projects.lan    IN   A   10.22.33.2
    
    romain.lan      IN   A   10.22.33.6
    simon.lan       IN   A   10.22.33.10
    vincent.lan     IN   A   10.22.33.14
    
    $ORIGIN example.com.
    

    Il ne reste plus qu'à relancer bind…

    # /etc/init.d/bind9 restart
    

    Apache

    Petit extra, si vous souhaitez configurer certains vhosts apache pour n'écouter que sur le VPN, vous devez d'une part définir le NameVirtualHost suivant 

    NameVirtualHost 10.22.33.1:80
    

    Ensuite, la déclaration du vhost se fait comme suit :

    <VirtualHost 10.22.33.1:80>
        ServerName git.lan.example.com
    
        # ...
    </VirtualHost>
    

    Sachez que pour les vhosts que vous souhaitez voir écouter sur les deux interfaces, vous allez devoir faire quelque chose qui semble redondant :

    <VirtualHost *:80 10.22.33.1:80>
        ServerName www.example.com
    
        # ...
    </VirtualHost>
    

    weboob / RMLL 2013

    Pour cette 14e édition des Rencontres Mondiales du Logiciel Libre, une partie de l'équipe de weboob était présente pour partager autour du projet.

    Bilan : deux conférences, programmées exactement au même moment par le plus grand des hasards, ainsi qu'une interview radio.

    Radio RMLL

    Noé et theo ont été interviewé par Radio RMLL en introduction à leur conférence.

    Écouter l'interview

    Weboob - Le Web en dehors du navigateur

    Présentation générale de weboob par theo et Noé, parsemée de trolls, avec en bonus une démonstration live sur les dix dernières minutes qui n'étaient pas prévues.

    Les banques françaises séquestrent vos données

    Romain s'est concentré sur l'aspect bancaire de weboob et sur les mécanismes employés par les banques pour empêcher les robots de scrapping d'exister.

    weboob / Weboob au CCC

    Weboob était présent au 29C3 cette année à Hambourg, en Allemagne, le temps d'un bref lightning talk présenté par Olf, Phlogistique et theo :

    /projects/weboob/Weboob-29c3-2012-12-30.mp4

    Atom Heart Mother

    Le 12 janvier dernier avait lieu au Théâtre du Châtelet une interprétation, enregistrée par France Inter, de la pièce Atom Heart Mother, des Pink Floyd, morceau de 25 minutes vraiment magnifique intégrant un orchestre, une chorale, mais aussi des instruments électriques.

    Interprétée par l’orchestre philharmonique de Radio France, avec la participation de Ron Geesin, co-compositeur de la pièce, une vidéo a été publiée. Malheureusement, la qualité sonore est médiocre, alors que l’enregistrement passé sur France Inter est beaucoup plus agréable.

    Afin d’allier le plaisir des yeux au plaisir des oreilles, j’ai fait un montage avec la bande son diffusée sur France Inter et la vidéo disponible sur Dailymotion. Ça donne ça :

    /media/Pink_Floyd_Atom_Heart_Mother.webm

    weboob / 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.

    BudgetInsight / Lancement

    Cela fait près d'un an que j'ai rejoint Clément Coeurdeuil et Mathieu Lordon pour créer la société Budget Insight qui édite l'application web du même nom permettant de gérer ses comptes bancaires.

    Le principe est de centraliser l'ensemble de ses comptes, de catégoriser les transactions, de créer des budgets, de gérer les remboursements, d'envoyer des alertes sur les mouvements, de détecter les dépenses et revenus fixes et de prédire l'évolution de son solde au cours du mois à venir, le tout automatiquement. Notre objectif est d'agir comme un conseiller virtuel en analysant et traitant vos données pour en sortir toutes les informations utiles que vous deviez jusqu'à présent gérer à la main.

    Comme vous pouvez vous en douter, nous utilisons weboob pour synchroniser les comptes bancaires de nos utilisateurs. Ce qui fait de Budget Insight un gros contributeur de ce logiciel libre sous licence AGPL, notamment pour la maintenance et l'écriture de nouveaux modules bancaires.

    Ce lundi a eu lieu le lancement officiel de l'application auprès du grand public, après des mois de béta-testing avec plus d'une centaine de personnes actives. Ceci marque l'aboutissement d'intenses développements, mais aussi le début de nouvelles problématiques, telles qu'une bonne gestion de la montée en charge, effectuer de la communication pour nous faire connaître auprès du grand public, la mise à disposition prochaine d'une application mobile, etc.

    Nous sommes persuadés que des applications comme la notre prendront en 2013 une part importante dans le quotidien des gens, dès lors que nous aurons réussi à faire sauter la barrière psychologique de faire confiance à un service tiers pour gérer ses données bancaires.

    Aussi n'hésitez pas à vous inscrire pour essayer, et à faire du prosélytisme autour de vous si vous êtes convaincu de l'apport d'une telle solution !

    weboob / DRM de NolifeTV

    Le site Nolife TV permet de visualiser les émissions de la chaîne de télévision du même nom via un player flash. Il existe un module Weboob pour se passer de logiciel propriétaire, mais comme on va le voir, ils ont mis en place un mécanisme afin d'empêcher le contournement. En vain.

    Première version

    Fin 2011 je suis tombé sur ce post de leur forum, qui inclus cette mention :

    Merci de ne pas intégrer la lecture des vidéos de Nolife Online dans votre client NoAir sans passer par l'interface web de Nolife Online. De façon générale, ne développez pas de lecteur accédant directement aux vidéos de Nolife Online sans notre accord.

    Y voyant une provocation au libriste que je suis, et bien que n'étant pas intéressé par les émissions de cette chaîne, je me suis mis en tête de pondre un module weboob. À des fins de recherche, évidemment.

    L'analyse du site m'a permis de constater que le player flash faisait deux requêtes à /_newplayer/api/api_player.php :

    skey=9fJhXtl%5D%7CFR%3FN%7D%5B%3A%5Fd%22%5F&connect=1&a=US
    

    Celle-ci effectue un connect=1 pour s'annoncer, je ne sais pas trop pourquoi.

    skey=9fJhXtl%5D%7CFR%3FN%7D%5B%3A%5Fd%22%5F&a=UEM%7CSEM&quality=0&id%5Fnlshow=1234
    

    Cette requête renvoie entre autres l'URL du fichier vidéo. On constate dans les paramètres le champ id_nlshow qui contient l'ID de la vidéo. Rien de bien compliqué, donc, mis à part cette skey dont je n'arrivais pas à déterminer l'origine.

    Après diverses recherches, j'ai fini par comprendre qu'il s'agissait d'une constante. Un peu déçu de ne pas y avoir pensé alors que c'est une « protection » commune, et sans comprendre comme d'habitude l'intérêt de faire ça, j'avais atteint une version fonctionnelle de mon module et savourais ma première victoire.

    Seconde version

    Il y a quelques mois, une nouvelle version du site de Nolife TV a été mise en ligne, cassant sans ménagement le module de weboob. N'ayant alors pas le courage de me replonger sur le code d'un module que je n'utilise pas, j'ai laissé les choses trainer… jusqu'à aujourd'hui.

    Je n'ai pas été déçu du voyage. Après avoir corrigé l'authentification, la recherche et autres trivialités, je me suis alors penché sur la récupération de l'URL des vidéos.

    Le player effectue maintenant non pas deux mais trois appels à /_nlfplayer/api/api_player.php :

    skey=1b6cf46e6e484e03b370f528441357fd&a=MD5&timestamp=1351359574
    

    Renvoie des paramètres qui ne semblent pas être utilisés par la suite, probablement l'équivalent du connect=1 de la précédente version.

    a=EML&skey=893873357e374594eb6fac475846574b&id%5Fnlshow=30833&timestamp=1351359576
    

    Récupère quelques méta-informations sur la vidéo.

    quality=0&a=UEM%7CSEM%7CMEM%7CCH%7CSWQ&skey=005f9ae80d77db93b557cc14c404aa76&id%5Fnlshow=30833&timestamp=1351359579
    

    Enfin, le graal, l'URL de la vidéo.

    Facile me diriez-vous ? Ce n'est sans compter un petit détail : le paramètre skey change d'un appel à l'autre. On constate également la présence du paramètre timestamp, ce qui laisse immédiatement penser que la clef est calculée à partir de lui. Mais de quelle manière ?

    C'est probablement un checksum MD5, et il y a fort à parier qu'une clef privée est utilisée comme salt. Et pour la retrouver, ça risque d'être coton.

    Je me suis alors penché sur divers outils pour tenter d'analyser le player flash. swftools, flasm ou flare n'en sont pas venu à bout. J'ai juste été capable de décompresser le .swf, mais aucune information dans ce qui restait un binaire ne m'ont donné la solution.

    C'est alors que je suis tombé sur ce site qui m'a sorti ce fichier parfaitement lisible.

    On y découvre les lignes suivantes :

    public function nl_dataloader(){
        this.loaded_data = new Object();
        super();
        salt = chaine([97, 53, 51, 98, 101, 49, 56, 53, 51, 55, 55, 48, 102, 48, 101, 98, 101, 48, 51, 49, 49, 100, 54, 57, 57, 51, 99, 55, 98, 99, 98, 101]);
    }
    public static function addKey(_arg1:URLVariables):URLVariables{
        _arg1.timestamp = new Date().time;
        _arg1.skey = getKey(_arg1.timestamp);
        return (_arg1);
    }
    public static function getKey(_arg1):String{
        return (MD5.encrypt((MD5.encrypt(String(_arg1)) + salt)));
    }
    

    À partir de là, la solution est évidente. Transposé en python dans le module weboob, ça donne le code suivant :

    SALT = 'a53be1853770f0ebe0311d6993c7bcbe'
    def genkey(self):
        timestamp = str(int(time.time()))
        skey = md5(md5(timestamp).hexdigest() + self.SALT).hexdigest()
        return skey, timestamp
    

    Et ça marche. Le module NolifeTV fonctionne de nouveau. Le commit complet pour régler le problème est visible ici.

    Conclusion

    Comme toujours, les mesures de protection que tentent de mettre en place les sites de streaming video se contournent très facilement. Il n'est plus à démontrer que les seules personnes que ça emmerde, ce sont les utilisateurs honnêtes qui ne peuvent pas regarder leur contenu payant sur la plateforme de leur choix, ni de les voir offline.

    weboob / Vérification des certificats

    Jusqu'alors, un problème dans la sécurité de Weboob est qu'il ne vérifie pas la validité du certificat envoyé par le serveur lorsqu'on établie une connexion SSL. Ceci est dû à l'utilisation de mechanize, la bibliothèque qui simule le comportement d'un navigateur, et qui n'offre pas de tel mécanisme.

    L'écriture par Laurent du Browser 2 se passant de mechanize pour le remplacer astucieusement par requests devrait résoudre ce problème proprement dans une prochaine version de Weboob. Malheureusement, ce nouveau browser n'étant pas encore terminé, une solution alternative et provisoire a été proposée par Florent.

    Comme mechanize a une gestion opaque du SSL (du fait des multiples couches le séparant de la bibliothèque openssl, et de la médiocrité du code), l'idée est d'établir une première connexion en utilisant directement openssl et de valider le certificat à partir du fingerprint préalablement renseigné dans le module, avant de poursuivre le déroulement normal via mechanize.

    Cette mesure de protection peut facilement être contournée, puisqu'il suffit à l'attaquant, dans le cas d'un Man-in-the-middle, de relayer la première connexion, et de s'interposer pour les suivantes qui elles ne seront pas vérifiées par mechanize.

    Néanmoins, ce mécanisme temporaire est efficace dans la plupart des cas.

    Pour les développeurs de modules, il suffit de renseigner au BaseBrowser l'attribut de classe CERTHASH contenant le SHA-256 de la chaîne de certificats du serveur :

    class BNPorc(BaseBrowser):
        DOMAIN = 'www.secure.bnpparibas.net'
        PROTOCOL = 'https'
        CERTHASH = '5511f0ff19c982b6351c17b901bfa7419f075edb13f2df41e446248beb7866bb'
    

    Une autre façon de faire est d'appeler directement la méthode StandardBrowser.lowsslcheck :

    browser.lowsslcheck('www.secure.bnpparibas.net', '5511f0ff19c982b6351c17b901bfa7419f075edb13f2df41e446248beb7866bb')
    

    Afin de calculer ce fingerprint, vous pouvez aisément procéder de la manière suivante :

    $ python
    Python 2.7.3rc2 (default, Apr 22 2012, 22:30:17)
    [GCC 4.6.3] on linux2
    Type "help", "copyright", "credits" or "license" for more information.
    >>> import ssl
    >>> from hashlib import sha256
    >>> domain = 'www.secure.bnpparibas.net'
    >>> sha256(ssl.get_server_certificate((domain,  443))).hexdigest()
    '5511f0ff19c982b6351c17b901bfa7419f075edb13f2df41e446248beb7866bb'
    

    Bien évidement, il vaut mieux être certain de ne pas soi-même être victime d'une attaque à ce moment là, car aucune vérification n'est effectuée.

    Il ne s'agit que d'une mesure temporaire, en attendant mieux dans le Browser 2.

    weboob / RMLL 2012

    Cette année se sont déroulées les Rencontres Mondiales du Logiciel Libre à Genève.

    Pour la première fois, Weboob avait son stand, conjointement à celui de Salut à Toi, ce qui permit non seulement de faire connaître le projet à de nombreuses personnes et d'avoir moult conversations intéressantes, mais aussi et surtout d'avoir un lieu pour poser mes fesses.

    Comme il y a deux ans, j'ai donné une conférence au sujet de Weboob. Bien que je l'ai très peu préparée et qu'il y avait moins de monde que la dernière fois, ça s'est plutôt bien passé.

    Sont disponibles la vidéo et les slides.

    Enfin, concernant Genève même, tout est effectivement très cher, il n'y a pas particulièrement de spécialités culinaires, ce qui fait que j'ai mangé italien, chinois, français… Quant au critère discriminant pour moi, à savoir la bière, il faut savoir qu'il est juste impossible de trouver autre chose que de la pisse.