Puyb Inside

vendredi 11 décembre 2009

Open Wifi Auto Connect

Je ne sais pas si vous utilisez souvent des réseaux wifi qui filtrent l'accès en utilisant un portail de capture web pour vous authentifier ? Personnellement, j'utilise régulièrement le service FreeWifi de Free...

Le principe de ces services est que le pare-feu bloque toutes les connexions qui ne sont pas authentifiés. Pour s'authentifier, il faut utiliser un navigateur Web, tenter de naviguer vers un site quelconque pour se faire rediriger vers la page d'authentification puis y entrer ses identifiants...

Comme je suis toujours plus fainéant, je viens de faire un petit script qui se charge de faire toutes ses étapes automatiquement. Le principe est de surveiller les connexions aux différents réseaux sans fils, et dès qu'un réseau est reconnu (que son nom est dans une liste), il se charge tout seul de récupérer la page du portail, puis de la renvoyer accompagnée des identifiants de connexion.

Utilisation :

  1. Téléchargez le fichier.
  2. Copiez le dans le dossier de votre choix.
  3. Créez le fichier de configuration. Il se nomme .OpenWifiAutoConnect, doit être placer dans votre dossier personnel (~) et est au format .ini. Les sections correspondent aux nom des réseaux. Les clefs correspondent au champs du formulaire ou il faut entrer une valeur. Par exemple:
    [UnReseau]
    email=test@example.com
    pass=azerty
    
    [FreeWifi]
    login=1234567890
    password=secret
    
  4. Ajoutez le programme au démarrage de votre session.
  5. Vérifier que les paquets (ubuntu / debian) suivant sont installé : python-dbus, python-notify et python-beautifulsoup
  6. Redémarrer votre session pour que le programme se lance
  7. Connectez vous à un réseau wifi et attendez que la magie s'opère. Une popup de notification vous préviendra du succès de l'authentification.

Comment ça marche ?


Avant tout, voici le code :
#!/usr/bin/env python

################################################################################
# OpenWifiAutoConnect - Stephane PUYBAREAU (puyb <at> puyb <dot> net) - 2008   # 
# Fully automated authentication to capture portal based wifi networks.        # 
# Config file: ~/.OpenWifiAutoConnect - Format: ini                            #
# Section are network SSID (names)                                             #
# key / value pair define user submited information (based on the html form)   #
#                                                                              # 
# This software is provided under the terms of the GPL v3 licence.             # 
# See http://www.gnu.org/licenses/gpl.html for more information                # 
#                                                                              # 
# This software use python dbus bindings, pynotify and BeautifulSoup modules   #
################################################################################


# The url the program will try to open hoping to be redirected to the portal
URL = 'http://perdu.com/'

import sys
import gobject
import dbus
import dbus.mainloop.glib
import urllib2, urllib
from BeautifulSoup import BeautifulSoup
import re
import ConfigParser
import os
import pynotify

def properties_changed_signal_handler(props):
    if not props.has_key('ActiveConnections'):
        return
    for device_path in props['ActiveConnections']:
        device = bus.get_object('org.freedesktop.NetworkManager', device_path)
        device_props = device.GetAll("org.freedesktop.NetworkManager.Connection.Active", dbus_interface="org.freedesktop.DBus.Properties")
        if device_props['Default']:
            return
        ap_path = device_props['SpecificObject']
        if ap_path.startswith('/org/freedesktop/NetworkManager/AccessPoint/'):
            ap = bus.get_object('org.freedesktop.NetworkManager', ap_path)
            ssid = ap.Get("org.freedesktop.NetworkManager.AccessPoint", "Ssid", dbus_interface="org.freedesktop.DBus.Properties")
            ssid = ''.join([chr(c) for c in ssid])
            if ssid not in config.sections():
                return
            print ssid
            device.connect_to_signal("PropertiesChanged", device_properties_changed_signal_handler(ssid), dbus_interface="org.freedesktop.NetworkManager.Connection.Active")

def device_properties_changed_signal_handler(ssid):
    def handler(props):
        if not props.has_key('State'):
            return
        if props['State'] != 2:
            return
        print ssid
        section = dict(config.items(ssid))

        if login(section):
            txt = "Successfully logged on " + ssid
        else:
            txt = "Failled to log on " + ssid
        n = pynotify.Notification("Open Wifi Auto Connect", txt, "dialog-warning")
        n.set_urgency(pynotify.URGENCY_NORMAL)
        n.set_timeout(10)
        #n.add_action("clicked","Button text", callback_function, None)
        n.show()
    return handler


def login(values):
    # build an http fetcher that support cookies
    opener = urllib2.build_opener( urllib2.HTTPCookieProcessor() )
    urllib2.install_opener(opener)

    # try to open the portal page
    f = opener.open(URL)
    data = f.read()
    redirect_url = f.geturl().split('?')[0]
    f.close()
    if redirect_url == URL:
        return # Our request wasn't hijacked by the portal (maybe the wifi network isn't the default connection)

    # parse the portal page
    soup = BeautifulSoup(data)
    form = soup.find('form')
    if not form:
        return # There's no form on this page
    
    # creating the post values
    login_post = {}
    for input in form.findAll('input'):
        if input.has_key('name'):
            default = ''
            if input.has_key('type') and input['type'] == 'checkbox':
                default = 'on'
            login_post[input['name']] = input.has_key('value') and input['value'] or default

    login_post.update(values)

    # guessing the post url
    if not form.has_key('action'):
        url = redirect_url
    elif not form['action'].startswith('/'):
        url = '/'.join(redirect_url.split('/')[:-1]) + '/' + form['action']
    else:
        url = '/'.join(redirect_url.split('/')[:3]) + form['action']

    # GET ou POST ?
    postBody = None
    if form.has_key('method') and form['method'].lower() == 'post':
        postBody = urllib.urlencode(login_post)
    else:
        url += '?' + urllib.urlencode(login_post).replace(' ', '+')

    # submit the form
    f = opener.open(url, postBody)
    data = f.read()
    f.close()

    # Test if the login was a success
    f = opener.open(URL)
    data = f.read()
    status = f.geturl() == URL
    f.close()
    return status

if __name__ == '__main__':
    config = ConfigParser.RawConfigParser()
    config.read(os.path.expanduser('~/.OpenWifiAutoConnect'))

    pynotify.init( "Open Wifi Auto Connect" )

    dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)

    bus = dbus.SystemBus()
    nm = bus.get_object("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager")
    nm.connect_to_signal("PropertiesChanged", properties_changed_signal_handler, dbus_interface="org.freedesktop.NetworkManager")

    loop = gobject.MainLoop()
    loop.run()


Le programme utilise D-Bus pour demander au NetworkManager de lui signaler chaque changement au niveau du réseau. A chaque changement, il tente de déterminer, toujours grâce à D-Bus, si ce reseau est un réseau Wifi et quel est son SSID. Si le réseau est dans le fichier de configuration, il attend que que le réseau soit correctement connecté pour lancer la fonction login avec les paramètres de connexion.

Cette fonction commence par mettre en place un client HTTP supportant les cookies (il est possible (FreeWifi le fait) que le portail nous envoie des cookies). Il tente d'ouvrir une page vers un site quelconque (en l'occurrence perdu.com. Le client supporte les redirections HTTP, il teste donc si la requette a été redirigé. Si c'est le cas, le HTML est analysé avec Beautiful Soup pour récupérer toutes les informations sur le formulaire. Elle sont modifiées avec les informations de connexion provenant du fichier de configuration. Puis, le formulaire est envoyé. Enfin, il vérifie si l'authentification à réussi en tentant de joindre à nouveau perdu.com...

Limitations :

  • Ce programme ne fonctionnera pas si le SSID du réseau n'est pas toujours le même. J'ai cru comprendre que les réseaux Fon n'avait pas tous le même nom, si c'est le cas, ça ne fonctionnera pas (enfin, pas pour tous les réseau Fon, mais juste pour ceux dont on a donner les noms.
  • Ce programme ne fonctionnera pas non plus si les noms des champs à remplire dans le formulaire sont variables.
  • Enfin, ça ne fonctionnera pas non plus si il y a un captcha (une joli image où l'on doit jouer à Champolion pour deviner ce qu'il ya d'inscrit).


Voilà, j'espère que ça va servir à quelqu'un (d'autre que moi)... Si il y a un problème, n'hésitez pas à laisser un commentaire... Et si il n'y a pas de problème, vous pouvez aussi laisser un commentaire ;-)

jeudi 10 décembre 2009

J'aime pas le perl

Mais je suis obligé de bosser dessus...

J'ai horreur cette syntaxe si particulière qui donne l'impression que Larry Wall a absolument tenu à utiliser les 94 symboles de la table ASCII...
Pourquoi diable se faire chier à différentier les variables en fonction de leur type avec des $, @ et autre % ?
Qui a besoin de systématiquement aplatir les tableaux imbriqué et les arguments ?
Pourquoi emmerder les développeurs avec une distinction entre référence et variable ? Peut être par nostalgie du C...
Et surtout, voici un petit exemple qui montre à quel point ce langage est tordu :
use strict;
use Data::Dumper;

my %h = (
    'a' => (
        'b' => 'c',
    ),
);
print Dumper(\%h);

Il y a une erreur dans ce code. J'ai utilisé des () au lieu des {} pour le deuxième hash. Ce qui fait que je n'ai pas une référence vers mon deuxième hash... N'importe quel langage aurai levé une exception devant une tel erreur... Au lieu de cela, Perl affiche tout fièrement :
$VAR1 = {
          'c' => undef,
          'a' => 'b'
        };
Il à aplatit le hash en décalant tout ! Hallucinant ! Qui peut bien avoir besoin d'une telle "fonctionnalité" ?

La bonne solution est :
use strict;
use Data::Dumper;

my %h = (
    'a' => {
        'b' => 'c',
    },
);
print Dumper(\%h);

Ce qui donne :
$VAR1 = {
          'a' => {
                   'b' => 'c'
                 }
        };

Je ne vois aucune raison pour justifier une telle complexité !