Référendum ADP: l'anti-captcha

Voici un script qui remplira automatiquement les captchas sur referendum.interieur.gouv.fr. Jamais un captcha n'aura fait autant parler de lui... et jamais son utilité n'aura été aussi discutable.

Le cauchemar de tous ceux qui suivent la pétition Le cauchemar de tous ceux qui suivent la pétition

Voici un script qui remplira automatiquement les captchas sur referendum.interieur.gouv.fr. Jamais un captcha n'aura fait autant parler de lui... et jamais son utilité n'aura été aussi discutable.

C'est le fameux "en même temps": le listing des signataires est public, mais il est en même temps kafkaïen à consulter (essayez de vous rechercher pour voir, de mon côté 11 captchas à saisir correctement pour me retrouver). Le listing est public, mais en même temps aucun comptage n'est transmis. L'initiative est nationale, mais en même temps aucune communication institutionnelle dans les médias. Tout ceci est à l'image de la République en marche: une communication maîtrisée, impeccable et inoffensive sur la forme pour un jeu subtil -pervers- avec les mots, une mauvaise foi militante et des objectifs peu glorieux dans le fond.

Mon script est donc ce qu'on appelle un Userscript, c'est à dire un script (un programme) permettant d'étendre les fonctionnalités natives, ici d'un navigateur. Pour utiliser un Userscript et donc ce script, il faut installer sur votre navigateur une extension (Greasemonkey pour Firefox ou Tampermonkey pour Chrome, ce sont les plus connues) puis ajouter dans celle-ci le script ci-dessous. Je ne fais pas de tutoriel spécifique sur ces étapes, vous trouverez tout ce qu'il faut sur Google mais à noter qu'il s'adresse à des utilisateurs ayant de solides connaissances car il va falloir créer une clef d'accès à une API de Google.

L'idée de mon approche est de pouvoir résoudre les captchas directement dans le navigateur et donc de le rendre potentiellement utilisable par n'importe qui. Je ne publie ici que le résolveur de captcha mais mon script complet permet d'automatiser entièrement la récupération des signataires, lettre par lettre. Techniquement, le script copie en mémoire le captcha tel qu'affiché sur le site, il colorise les contours des lettres et des chiffres puis il nettoie le "bruit" (tous les pixels parasites). On obtient ainsi le captcha simplifié, qui ressemble à ça:

Contrairement aux apparences, ceci n'a pas été écrit avec mon sang Contrairement aux apparences, ceci n'a pas été écrit avec mon sang

Un appel à l'API Google Vision est enfin fait pour procéder à une reconnaissance de caractères. Le fait de nettoyer le captcha auparavant est obligatoire, sinon la reconnaissance échoue à chaque fois. Avec ce process, cela fonctionne environ 1 fois sur 5 (c'est assez variable), quand il échoue à reconnaître une nouvelle tentative est immédiatement lancée. A noter que l'API Google Vision n'est gratuite que pour les 1000 premiers appels par mois, ensuite c'est facturé à raison de $1.5 par 1000 unités. Voilà pourquoi je ne peux pas diffuser ma clef et qu'il vous faudra créer la vôtre. Cependant si plusieurs personnes utilisent ce principe, un compteur collaboratif pourrait tourner quasiment 24H/24 sans frais pour personne et ce de manière autonome (il suffit d'une clef API Google Vision, un onglet de votre navigateur avec l'userscript installé et le site du référendum ouvert).

PS: c'est un code bricolé en 4 heures hier soir, je ne prétends absolument pas que l'approche soit la meilleure et j'admets qu'il peut piquer les yeux mais ça fonctionne. J'ai un travail à côté, des enfants, tout ça :)

PS2: il est indispensable de se mobiliser mais attention à ne pas se faire piéger: que ça ne devienne pas un os à ronger le temps de préparer les autres privatisations évoquées (FdJ, etc). Privatisations qui d'ailleurs n'étaient pas dans le programme sur lequel a été élu Emmanuel Macron.

Voici le code de l'Userscript en question:

// ==UserScript==
// @name Referendum ADP anti-captcha
// @namespace http://tampermonkey.net/
// @version 0.1
// @description Résolveur du captcha referendum.interieur.gouv.fr
// @author Dimitri
// @match https://www.referendum.interieur.gouv.fr/*
// @grant unsafeWindow
// @require http://code.jquery.com/jquery-latest.js
// @run-at document-start
// @grant GM_xmlhttpRequest
// ==/UserScript==

$(window).load(function() {
if ($('#captcha').length) {
var img = document.getElementById('captcha');
var canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
canvas.getContext('2d').drawImage(img, 0, 0, img.width, img.height);
for (var y = 0; y < 120; y++) {
for (var i = 0; i < 400; i++) {
var pixel = canvas.getContext('2d').getImageData(i, y, 1, 1).data;
var pixeln = canvas.getContext('2d').getImageData(i, (y - 1), 1, 1).data;
var pixele = canvas.getContext('2d').getImageData((i + 1), y, 1, 1).data;
var pixelen = canvas.getContext('2d').getImageData((i + 1), (y - 1), 1, 1).data;
var pixeles = canvas.getContext('2d').getImageData((i + 1), (y + 1), 1, 1).data;
var pixels = canvas.getContext('2d').getImageData(i, (y + 1), 1, 1).data;
var pixelo = canvas.getContext('2d').getImageData((i - 1), y, 1, 1).data;
var pixelon = canvas.getContext('2d').getImageData((i - 1), (y - 1), 1, 1).data;
var pixelos = canvas.getContext('2d').getImageData((i - 1), (y + 1), 1, 1).data;
var scoring = 0;
if (pixeln[0] == 140) {
scoring++;
}
if (pixele[0] == 140) {
scoring++;
}
if (pixels[0] == 140) {
scoring++;
}
if (pixelo[0] == 140) {
scoring++;
}
if (pixelen[0] == 140) {
scoring++;
}
if (pixeles[0] == 140) {
scoring++;
}
if (pixelon[0] == 140) {
scoring++;
}
if (pixelos[0] == 140) {
scoring++;
}
if (
(
((pixel[0] >= 100 && pixel[0] <= 200) && scoring >= 4)
)
) {
var ctx = canvas.getContext('2d');
ctx.fillStyle = "rgb(200,0,0)";
ctx.fillRect(i, y, 1, 1);
}
}
}

for (var y = 0; y < 120; y++) {
for (var i = 0; i < 400; i++) {
var pixel = canvas.getContext('2d').getImageData(i, y, 1, 1).data;
var pixeln = canvas.getContext('2d').getImageData(i, (y - 1), 1, 1).data;
var pixele = canvas.getContext('2d').getImageData((i + 1), y, 1, 1).data;
var pixelen = canvas.getContext('2d').getImageData((i + 1), (y - 1), 1, 1).data;
var pixeles = canvas.getContext('2d').getImageData((i + 1), (y + 1), 1, 1).data;
var pixels = canvas.getContext('2d').getImageData(i, (y + 1), 1, 1).data;
var pixelo = canvas.getContext('2d').getImageData((i - 1), y, 1, 1).data;
var pixelon = canvas.getContext('2d').getImageData((i - 1), (y - 1), 1, 1).data;
var pixelos = canvas.getContext('2d').getImageData((i - 1), (y + 1), 1, 1).data;
if (pixel[0] != 200 || (pixeln[0] != 200 && pixele[0] != 200 && pixels[0] != 200 && pixelo[0] != 200 && pixelen[0] != 200 && pixeles[0] != 200 && pixelon[0] != 200 && pixelos[0] != 200)) {
var ctx = canvas.getContext('2d');
ctx.fillStyle = "rgb(255,255,255)";
ctx.fillRect(i, y, 1, 1);
}
}
}

for (var y = 0; y < 120; y++) {
for (var i = 0; i < 400; i++) {
var pixel = canvas.getContext('2d').getImageData(i, y, 1, 1).data;

var pixele1 = canvas.getContext('2d').getImageData((i + 1), y, 1, 1).data;
var pixelo1 = canvas.getContext('2d').getImageData((i - 1), y, 1, 1).data;
var pixels = canvas.getContext('2d').getImageData(i, (y + 1), 1, 1).data;
var pixeln = canvas.getContext('2d').getImageData(i, (y - 1), 1, 1).data;
var pixelon = canvas.getContext('2d').getImageData((i - 1), (y - 1), 1, 1).data;
var pixele = canvas.getContext('2d').getImageData((i + 1), y, 1, 1).data;
var pixelo = canvas.getContext('2d').getImageData((i - 1), y, 1, 1).data;

if (pixel[0] == 255 && pixele1[0] == 200 && pixelo1[0] == 200) {
var ctx = canvas.getContext('2d');
ctx.fillStyle = "rgb(200,0,0)";
ctx.fillRect(i, y, 1, 1);
}

if (pixel[0] == 255 && pixelon[0] == 200 && pixels[0] == 200) {
var ctx = canvas.getContext('2d');
ctx.fillStyle = "rgb(200,0,0)";
ctx.fillRect(i, y, 1, 1);
}

if (pixel[0] == 255 && pixeln[0] == 200 && pixels[0] == 200) {
var ctx = canvas.getContext('2d');
ctx.fillStyle = "rgb(200,0,0)";
ctx.fillRect(i, y, 1, 1);
}
}
}

for (var y = 0; y < 120; y++) {
for (var i = 0; i < 400; i++) {
var pixel = canvas.getContext('2d').getImageData(i, y, 1, 1).data;
var pixels = canvas.getContext('2d').getImageData(i, (y + 1), 1, 1).data;
if (pixel[0] == 200 && pixels[0] == 255) {
var x = y;
x++;
while (pixels[0] != 200) {
if ((x - y) > 8) {
break;
}
pixels = canvas.getContext('2d').getImageData(i, x, 1, 1).data;
x++;
}
if ((x - y) <= 8) {
var ctx = canvas.getContext('2d');
ctx.fillStyle = "rgb(200,0,0)";
ctx.fillRect(i, y, 1, (x - y));
}
}

}
}

var img = canvas.toDataURL();
img = img.replace('data:image/png;base64,', '');
var request = {
"requests": [{
"image": {
"content": img
},
"features": [{
"type": "TEXT_DETECTION",
"maxResults": 1
}]
}]
};
GM_xmlhttpRequest({
method: 'POST',
responseType: 'json',
url: 'https://vision.googleapis.com/v1/images:annotate?key=VOTRE_CLEF_GOOGLE_API_ICI',
data: JSON.stringify(request),
onload: function(response) {
if (typeof response.response.responses[0].fullTextAnnotation !== 'undefined') {
var reponse = response.response.responses[0].fullTextAnnotation.text.split(/\n/);
$('#form_captcha').val(reponse[0].replace(/\s/g, ''));
$('.form-actions button').trigger('click');
} else {
location.reload();
}

}
})
}
});

Le Club est l'espace de libre expression des abonnés de Mediapart. Ses contenus n'engagent pas la rédaction.