Spam du web

Nuit Blanche event in Toronto, Canada
Image via Wikipedia

Tout le monde sait ce que sont les spam ou pourriels qui à l’instar des prospectus inondent les boîtes aux lettres électroniques. Cette nuisance qui parasite le trafic internet pousse à la consommation dans les cas les plus inoffensifs ou bien va jusqu’à la tentative d’arnaque.

Il existe un autre type de nuisance sur le web. Il s’agit de l’occupation même du web à travers ses pages et ses réseaux sociaux. Cette présence à sans doute plus d’impact que le spam classique. En effet occuper l’espace numérique crée un buzz qui touche de nombreuses personnes… nous oseront même dire touche celles qui comptent car celle-ci peuvent changer le cours de l’histoire. Un président américain l’a d’ailleurs bien compris tandis que d’autres ne l’ont pas compris et ne sont plus au pouvoir.

Internet est aujourd’hui le média qu’il faut investir comme outils de propagande. A cette fin des sociétés se proposent, monnayant finance, de créer du buzz ou de lifter une e-réputation. Nous noterons toutefois que si l’objectif de manipuler l’opinion est le même que celui d’une publicité sur papier ou à la télévision, la forme diffère grandement. Les meilleurs buzz sont ceux qui ne ressemblent pas à de la publicité. En effet l’opinion du web est plus sensible à lui même qu’à des messages extérieurs: une forme de bouche à oreille numérique.

Comme rien n’est plus ressemblant à l’original que l’original lui-même, créer un buzz nécessite beaucoup de moyens. Il faut investir les forum, créer des sites web de propagande, simuler de nombreux fans. Pourtant nous sommes bien dans un univers créé par les ordinateurs, il doit donc être possible d’automatiser le buzz. L’idée est d’automatiser la création de contenu sur de multiple site. Chaque instance devra autant faire se peut être différente (pas de copier/coller) de l’autre afin de simuler des rédacteurs humains différents. L’automatisation doit permettre non seulement de simuler le nombre mais aussi de créer l’historique car de nombreuses références qui n’ont pas d’historique sont moins crédibles qu’une référence ayant pignon sur le web depuis des lustres. Cela est réalisable assez facilement puisque les hébergeurs de blog tels que blogger.com ou wordpress.com exposent une API pour publier des billet de manière programmatique. La modifications des billets sans en altérer le sens pour qu’ils ressemblent à ceux qu’aurait pu faire un humain, peut être obtenue en remplaçant les mots par des synonymes piochés par exemples sur le site de l’université de Caen. Le plus difficile est la créations des comptes initiaux (compte email google@par exemple) qui signent l’acte de naissance de la vraie fausse identité. En effet les fournisseurs utilisent des filtres pour repousser la création de compte par des robots.

Le code suivant montre un POC. Quand à savoir si la méthode est efficace pour manipuler le référencement des moteurs de recherche, le lecteur ce lancera lui-même dans l’expérience.

#!/usr/bin/python
# -*- encoding: UTF-8 -*-
'''
Created on 24 avr. 2011

@author: thierry
'''

import os.path
import random
from optparse import OptionParser

from gdata import service
import gdata
import atom
import urllib
from HTMLParser import HTMLParser
import random

import logging


LEVELS = {'debug': logging.DEBUG,
          'info': logging.INFO,
          'warning': logging.WARNING,
          'error': logging.ERROR,
          'critical': logging.CRITICAL}

class TextCloner():
    '''
    This class is responsible for cloning a text e.g. rewrite the text modifying
    some words to simulate human copying.    
    '''
    def clone(self, original):
        pass

class SynonymeCloner(TextCloner):
    
    
    def insert_synonyme(self, before, synonyme, after):
        url = '//dictionnaire.tv5.org/dictionnaires.asp?%s'
        params = urllib.urlencode({'Action':'1', 'mot': synonyme.split(' ')[-1:][0].encode("utf-8")})
        f = urllib.urlopen(url % params)
        response = f.read().decode('iso-8859-1')
        f.close()
        voyelle = ['a','e','i','o','u','y','é']
        if synonyme[0] in voyelle 
            or ( synonyme[0] == 'h' and synonyme[1] in voyelle) :
            art = "l'" 
        elif response.find("masculin") > -1 :
            art = 'le '
        else:
            art = 'la '
        if before.endswith(". ") or len(before.strip()) == 0:
            art = art.capitalize()
        clone = before + art + synonyme + after            
        return clone
        
    def clone(self, original):
        clone = original
        start = 0;
        while start > -1:
            START = "<%"
            start = clone.find(START)
            if start > -1:
                before, start_tag, after = clone.partition(START)
                END = "%>"
                word, end_tag, after = after.partition(END)
                synonyme = self.get_synonyme(word)
                logging.info("choose synonyme %s" % synonyme)
                clone = self.insert_synonyme(before, synonyme, after)                                                
                
        return clone
    
    
    
    def get_synonyme(self, word):            
        logging.info("looking for synonyme for %s" % word)        
        f = urllib.urlopen('//www.crisco.unicaen.fr/des/synonymes/%s' % urllib.quote(word.encode("utf-8")))
        reponse = f.read().decode('utf-8')
        f.close()
        
        class MyParser (HTMLParser):
            result_found = False
            tag = None
            cdep = None
            cp_found = []
            grab_result = False

            def __init__(self):
                HTMLParser.__init__(self)
                self.tag = None
                self.result_found = False
                self.cp_found = []

            def handle_starttag(self, tag, attrs):
                if tag == "div":
                    for a, v in  attrs:
                        if a == 'id' and  v == 'synonymes':
                            self.result_found = True
                            self.tag = tag
                            self.tag_count = 1
                            return
            
                if self.result_found and tag == "div":
                    self.tag_count += 1
            
                if self.result_found and tag == "a":
                    self.grab_result = True
  
            def handle_endtag(self, tag):
                if self.result_found:
                    if tag == self.tag:
                        self.tag_count -=1
                        if self.tag_count == 0:
                            self.result_found = False
                    if tag == "a":
                        self.grab_result = False
 
            def handle_data(self, data):
                if self.result_found and self.grab_result:
                    self.cp_found.append(data)

        
        p = MyParser()
        p.feed(reponse)
        p.close()
        return random.choice(p.cp_found + [word])

    
class Blog():
    '''
    This class can post an entry to a blog
    '''
    def post(self, title, content):
        pass
    

class Blogger(Blog):
    '''
    This blog poster implement blogger API.
    '''
    def __init__(self, login, password, blog_url):
        self.blogger_service = service.GDataService(login, password)
        self.blogger_service.source = 'publisher'
        self.blogger_service.service = 'blogger'
        self.blogger_service.account_type = 'GOOGLE'
        self.blogger_service.server = 'www.blogger.com'
        self.blogger_service.ProgrammaticLogin()
        
        query = service.Query()
        query.feed = '/feeds/default/blogs'
        feed = self.blogger_service.Get(query.ToUri())

        print feed.title.text
        for entry in feed.entry:
            logging.info("t" + entry.title.text)
            for link in entry.link:
                if link.href.startswith(blog_url):
                    self.blog_id = entry.GetSelfLink().href.split("/")[-1]

                    

    
    def post(self, title, content):
        
        entry = gdata.GDataEntry()
        entry.title = atom.Title('xhtml', title)
        entry.content = atom.Content(content_type='html', text=content)
        return self.blogger_service.Post(entry, '/feeds/%s/posts/default' % self.blog_id)



class AccountParser():
    '''
    This class can read the account file
    '''
    def __init__(self, filename):
        self.filename = filename
    
    def account_iter(self):
        logging.info("parsing %s account file" % self.filename)
        with open(self.filename) as f:
            for line in f:
                splitted = line.split(",")
                url = splitted[0]
                login = splitted[1]
                password = splitted[2]
                if url.find("blogspot.com") > -1:  
                    logging.info("found blospot account %s" % url)                        
                    yield Blogger(login, password, url)
                else:
                    logging.warn("skip unsupported blog %s" % url)
    




def run():
    usage = "%prog -a <account> -p <post> [-l <log level>]"
    str_version = "%prog 0.1"
    parser = OptionParser(usage=usage, version=str_version)
    parser.add_option("-a", "--account", action="store", type="string", dest="account", help="account file")
    parser.add_option("-p", "--post", action="store", type="string", dest="post", help="post file")
    parser.add_option("-l", "--log", action="store", type="string", dest="level_name", help="log level")
    parser.add_option("-t", "--title", action="store", type="string", dest="title", help="post title")    
    options, args = parser.parse_args()

    level = LEVELS.get(options.level_name, logging.NOTSET)
    logging.basicConfig(level=level)
    
    with open(options.post) as post_file:
        post = post_file.read()       
        for blog in AccountParser(options.account).account_iter(): 
            if options.title:
                title= options.title
            else:
                title = ""       
            blog.post(title, SynonymeCloner().clone(post))

if __name__ == '__main__':    
    run()


Liste des codes postaux et codes insee des communes

Map showing the communes of Metropolitan Franc...
Image via Wikipedia

Le terme commune est utilisé de manière assez générique en France. Pour ma part je préfère considérer qu’une commune est la plus petite division administrative c’est-à-dire qu’une commune correspond à une mairie. Les communes de France sont identifiées par l’INSEE, organisme public, par un code. La liste des codes INSEE et donc des communes est disponible sur le site de l’INSEE dans la rubrique téléchargement des Code Officiels Géographiques. Le fichier intitulé Liste des communes existantes au 1er janvier 2011 nous intéresse ici.

Par ailleurs si les codes INSEE constituent une nomenclature officielle, ils sont aussi réservés aux initiés (statisticiens, cartographes…). Le grand public ne connait des communes que leur code postal. Malheureusement la notion de code postal ne correspond pas à la notion de commune. En effet sur une commune il peut y avoir plusieurs codes postaux. Un code postal peut par exemple se justifier quand une localité est éloignée du centre ville ou dans le cas d’arrondissement de grande ville. Les cas sont nombreux. La liste des codes postaux est gérée par La Poste, service d’origine publique. Pourtant il est difficile d’obtenir la liste des codes postaux et un marché de niche existe dans le domaine. Le Service National des Adresse (SNA) propose néanmoins un service en ligne des codes postaux. Ce dernier donne le code postal en fonction du nom de la localité.
Nous nous plaçons dans la problématique d’obtenir un référentiel des communes avec leur code INSEE et leur code postal. L’idée est de partir de la liste de l’INSEE puis de requêter le service du SNA. Ce dernier ne proposant pas de liste complète il faut pour chacune des communes remplir un formulaire pour obtenir le code postal correspondant.

Le script python suivant permet heureusement de faciliter ce travail en prenant en argument le fichier des codes INSEE et le fichier à générer. Les 2 difficultés sont:

  • Le format des noms de localité: pas de tiret, d’apostrophe etc.
  • La réponse HTML qui n’est pas bien formée: attribut sans délimiteur
#!/usr/bin/python
import urllib
import sys
from HTMLParser import HTMLParser
import logging

def get_cp(txtCommune,cdep=None):
  logging.info("looking for %s" %  txtCommune)
  params = urllib.urlencode({'txtCommune':txtCommune, 'selCritere':'CP'})
  f = urllib.urlopen('//www.laposte.fr/sna/rubrique.php3?id_rubrique=59&recalcul=oui', params)
  reponse = f.read().decode('iso-8859-1')
  f.close()
  #-- remove malformatted HTML
  i = 0
  while i > -1:
    i = reponse.find('onclick=window.open')
    if i > -1 :
      logging.warn("malformed html found")
      j = reponse.find('false;',i)
      reponse = reponse[:i] + reponse[j+len('false;'):]
  p = MyParser(cdep)
  p.feed(reponse)
  p.close()
  return p.cp_found

class MyParser (HTMLParser):
  result_found=False
  tag = None
  cdep = None
  cp_found = []

  def __init__(self, cdep):
    HTMLParser.__init__(self)
    self.cdep = cdep
    self.tag = None
    self.result_found = False
    self.cp_found= []

  def handle_starttag(self, tag, attrs):
    for a,v in  attrs:
      if a == 'class':
        if v  =='resultat':
           self.result_found=True
           self.tag = tag

  def handle_endtag(self, tag):
    if self.result_found:
      if tag == self.tag:
        self.result_found=False

  def handle_data(self,data):
    if self.result_found :
      if self.cdep:
        if data.startswith(self.cdep) :
          self.cp_found.append(data)
      else:
        self.cp_found.append(data)

def normalize_commune(art, ncc):
  logging.info("normalizing %s %s" % (art, ncc))
  resu=None
  if art:
    resu = art.strip('(').strip(')').strip("'")
  if resu:
    resu = "%s %s" % (resu, ncc)
  else:
    resu = ncc
  resu = resu.replace('-',' ').replace('SAINT','ST').replace("'"," ")
  return resu

logging.basicConfig(level=logging.INFO)
with open(sys.argv[2],'w') as insee_cp:
  with  open(sys.argv[1]) as insee:
    first = True
    for line in insee:
      if first:
         first = False
         continue
      splitted = line.split('t')
      dep = splitted[3]
      com = splitted[4]
      artmaj = splitted[8]
      ncc = splitted[9]
      txtCommune = normalize_commune(artmaj, ncc)
      cp = get_cp(txtCommune, dep)
      cd_insee = dep + com
      if len(cp) > 1:
        logging.warn("Plusieurs  CP")
      for a_cp in cp:
        logging.info("%s CP %s INSEE %s" % (txtCommune, a_cp, cd_insee))
        insee_cp.write("%st%st%s" % (txtCommune, a_cp, cd_insee))
        insee_cp.write("n")

Le fichier généré contient le nom de la commune (formatée pour le site du SNA), le code postal et le code INSEE. Le lecteur pourra sans peine modifier le script pour qu’il sorte le nom de la commune dans sa forme originale (avec les apostrophes, tirets etc.)

Pour ceux que le code python rebute, le résultat du script est par ici : code postal et insee des communes 2011

Générer sa carte de visite en QRcode

QR code exemple
Image via Wikipedia

Les QRCode sont des matrices de points plus riches que les codes barres mais suffisamment concis pour être interprété rapidement  grâce notement à une redondance qui corrige les erreurs.

La génération de qrcode est aisée sous linux où il existe l’outil qrencode disponible dans le dépôt Ubuntu. Les QRCode peuvent contenir du texte ou des données binaires. On peut donc y stocker une carte de visite au format vcard qui est du texte (qui peut contenir du binaire comme on le verra).

Notre carte de visite est saisie à un seul endroit disons dans notre machine de bureau qui est sous Ubuntu bien entendu. Dans les informations personnelles de l’utilisateur sont présentes toutes les données de la carte de visite. On va voir comment les extraires dans un vcard puis générer le qrcode correspondant.

Extraction du vcard

Sous Ubuntu c’est au bureau GNOME que nous nous adressons afin d’obtenir la carte de visite de l’utilisateur. Nous passons par le binding python de Evolution (suite communicante mail, agenda etc) et la bibliothèque vobject:

Voici le script myvcard.py :

#!/usr/bin/python
# -*- encoding: UTF-8 -*-
import evolution
c = evolution.ebook.get_self_contact()
v = c.get_vcard_string()
import vobject
vo = vobject.readOne(v)
# remove photo because too big for qrcode and N900 may not handle it
del vo.photo
print vo.serialize()

Ce script affiche sur la sortie standard la vcard de l’utilisateur.

Génération du qrcode

Maintenant que le contenu de la vcard est obtenu il suffit de générer le qrcode. Pour cela il suffit de lancer le script suivant, myvcard2qrcode.sh:

#!/bin/bash
./myvcard.py | qrencode -o $1

Il prend un argument qui est le fichier PNG de sortie. Par exemple:

./myvcard2qrcode.sh tb.png

va générer le fichier tb.png, image du qrcode !

Vérification de mail signé avec Javamail crypto

Icon from Nuvola icon theme for KDE 3.x.
Image via Wikipedia

L’utilisation de javamail crypto doit permettre au développeur de ne pas trop rentrer dans la technique de la norme S/MIME. En effet avec des points d’entrée clairs comme crypter, signer, décrypter et vérifier on fait rapidement confiance. A tord.

Il y a pourtant des signes qui ne trompent pas comme la date de dernière mise à jour du projet: juin 2006, et les quelques TODO dans le code. On se raccroche néanmoins à un espoir devant la peur du code à écrire sans l’aide de cette bibliothèque. De plus, quand il s’agit de crypter, signer et décrypter, Javamail Crypto donne satisfaction. Le seul point inquiétant est la vérification de la signature S/MIME qui nécessite de faire une petite gymnastique pour fournir à l’API la clé publique pour vérifier la signature…

En regardant le post Réception de mail crypté S/MIME, on voit qu’une fois le certificat publique obtenu, il suffit de la passer à la classe utilitaire en même temps que le mail dont on veut vérifier la signature. La confiance est là et on oublie de tester ce qui se passe quand on signe avec un certificat différent de celui utilisé pour la vérification. La réponse est rien.

Quelle fut ma déception quand un mail signé par un tiers fut validé par javamail crypto combien même je lui passe un certificat différents comme référence! En investiguant la méthode appelée et grace à l’open source, la crédulité dont on a pu faire prouve tire en nous un le gloussement d’un rire nerveux. La clé passée en paramètre n’est jamais utilisée !!!!

En fait javamail se contente de vérifier que la signature correspond bien au contenu signé. Cette vérification de la non altération du message est cependant insuffisante quand il s’agit de données sensible. Sans aller jusqu’à s’assurer de la non répudiation en vérifiant les listes de révocation, on s’attend à ce que Javamail crypto s’assure au moins que la signature provient bien d’un émetteur donné. Sinon à quoi ça sert de passer la clé publique ?

Finalement il vaut mieux s’adresser à Dieu qu’à ses saints. En utilisant directement Bouncycastle dont javamail crypto est une abstraction, on s’en sort. Avec plus de code mais il faut bien ça. Le lecteur trouvera e lui même le détail de la solution en se référant à la classe org.bouncycastle.mail.smime.examples.ValidateSignedMail présente dans les sources.

N900 : Read-only Filesystem

N900 desktop
Image by RafeB via Flickr

De temps à autre la partition montée sous /home/user/MyDocs passe en lecture seule. Cette partition en FAT est sujette à des erreurs surtout si on arrête le terminal de façon brutale : retrait de batterie, bouton off ?

Si cela arrive il suffit de démonter la partition pour la réparer.

Dans un terminal, passer en root
sudo gainroot

Démonter la partition
umount /home/user/MyDocs/

Réparer la partition en mode automatique
/home/user # fsck -y /dev/mmcblk0p1

Enfin rebooter pour laisser le système remonter la partition
reboot