Récupération des appels en absence sur box SFR Red

Je possède une box Internet avec abonnement fibre optique fournie par SFR Red. Je suis satisfait de cet abonnement, qui fournit selon moi un rapport qualité/prix imbattable (lorsque tout fonctionne bien). En particulier, c’est l’un des rares fournisseurs d’accès à Internet qui proposent des tarifs en promotion valables à vie… Et pas seulement la première année, avant d’être quadruplés les années suivantes. Mais il est vrai qu’en contrepartie, il faut accepter d’avoir affaire à un service client minimaliste inexistant.

La box en question est de type NB6 et utilise chez moi, au moment de la rédaction de cet article, le firmware NB6VAC-MAIN-R4.0.40.

Cet abonnement me fournit également une ligne téléphonique fixe. Je l’utilise encore régulièrement et j’aimerais être notifié lorsque je loupe un appel.

Il est possible d’activer le clignotement d’une LED de la box lorsqu’un appel a été manqué. Malheureusement, elle est enfermée dans un placard technique et cette LED n’est pas visible. Je préfèrerais recevoir un SMS.

Pour mettre en place cela, je tire profit de l’historique des appels qui est accessible sur l’interface de configuration de la box (accessible depuis l’adresse IP par défaut http://192.168.1.1 si vous ne l’avez pas modifiée). En effet, après s’être authentifié, une page Web affiche les derniers appels émis ou reçus :

Il suffit donc de charger régulièrement cette page et de regarder si un nouvel appel manqué s’est ajouté au tableau. Pour automatiser cela, je vais rajouter une tâche au programme écrit en C qui tourne sur mon serveur et qui gère l’arrière-plan de ma domotique.

Pour rapatrier la page Web, j’utilise cURL comme à chaque fois que j’ai des requêtes HTTP à exécuter. La page en question est http://192.168.1.1/voip/config, mais il n’est bien évidemment pas possible de l’appeler directement : il faut passer par une étape d’authentification pour avoir accès à l’interface Web de la box. Pour cela, deux possibilités :

  • Soit grâce à un couple utilisateur/mot de passe
  • Soit en appuyant sur le bouton physique WPS de la box

Dans mon cas, je dois donc me contenter de la première solution, même si cela implique d’inscrire en clair sur le serveur les identifiants de connexion (si vous avez une idée pour faire autrement n’hésitez pas à lâcher vos com’s).

Pour connaître les étapes à réaliser pour passer l’authentification par nom d’utilisateur et mot de passe, j’analyse donc le code source de la page Web pour trouver quels paramètres sont envoyés lors de la validation du formulaire.

Il en ressort que le formulaire d’authentification envoie les paramètres suivants via POST à l’adresse http://192.168.1.1/login :

  • « method » avec la valeur constante « passwd« 
  • « page_ref » qui désigne la page à charger une fois l’authentification effectuée. Dans mon cas, c’est donc « /voip/config« 
  • « zsid » sans aucune valeur
  • « hash » sans aucune valeur
  • « login » et « password » qui sont les champs textes contenant les identifiants
  • « submit_button » sans aucune valeur

Je valide donc que ces paramètres sont suffisants en créant un fichier HTML ne contenant que le strict nécessaire :

<form method="post" action="http://192.168.1.1/login">
<input type="hidden" name="method" value="passwd" />
<input type="hidden" name="page_ref" value="/voip/config" />
<input type="hidden" name="zsid" id="zsid" />
<input type="hidden" name="hash" id="hash" />

<input type="text" class="text" name="login" id="login" size="30" />
<input type="password" class="text" name="password" id="password" size="30" />

<button type="submit" name="submit_button" class="btn">Valider</button>
</form>

Après ouverture de ce fichier dans un navigateur et validation de ce formulaire, j’arrive directement sur la page qui m’intéresse. C’est parfait !

Je créé donc une requête semblable grâce à cURL :

curl -d 'method=passwd&page_ref=/voip/config&zsid=&hash=&login=[mon_identifiant]&password=[mon_mot_de_passe]&submit_button=' -X POST http://192.168.1.1/login

Qui semble bien fonctionner, car si je saisis des identifiants invalides, ma box me renvoie ici la page de connexion.

Avec les identifiants corrects, le serveur indique toutefois qu’il n’y a rien à voir ici et qu’une redirection doit être réalisée :

Il faut donc rajouter le flag -L pour indiquer à cURL de la suivre. Malheureusement, cela ne fonctionne pas : j’arrive sur une page qui m’indique que je ne suis pas (plus ?) authentifié.

Pour continuer, je dois chercher de quelle manière le navigateur conserve l’authentification durant tout le temps de la navigation sur l’interface de la box. Pour cela, je reprends ma page Web de test et je regarde la réponse du serveur avec l’analyseur de réseau de Firefox :

On voit que de nombreuses requêtes sont réalisées pour charger la page Web complète, mais la première (vers /login) répond avec un identifiant de session stockée dans un cookie. C’est donc celui-ci qu’il est nécessaire de renvoyer dans les requêtes suivantes.

Il faut alors rajouter l’option -c cookie.txt pour demander à cURL de stocker dans un fichier les cookies renvoyés par le serveur lors de l’authentification. En l’occurrence, je les place dans /tmp qui est chez moi un ramdisk afin de ne pas écrire inutilement dans la mémoire Flash du SSD :

curl -L -c /tmp/cookie.txt -d 'method=passwd&page_ref=/voip/config&zsid=&hash=&login=[mon_identifiant]&password=[mon_mot_de_passe]&submit_button=' -X POST http://192.168.1.1/login

Lors des requêtes suivantes, il faut rajouter -b cookie.txt pour à l’inverse renvoyer au serveur le contenu de ce cookie. Je conserve l’option -c au cas où le serveur mette à jour les informations stockées à l’intérieur :

curl -L -c /tmp/cookie.txt -b /tmp/cookie.txt http://192.168.1.1/voip/config

Je peux enfin récupérer le contenu de la page Web convoitée ! En particulier le tableau qui m’intéresse :

<div class="title">
    <h1>Historique d'appel</h1>
</div>
<table id="call_history_list">
	<thead>
		<tr>
			<th scope="col">Sens</th>
			<th scope="col">Type</th>
			<th scope="col">Numéro de téléphone</th>
			<th scope="col">Durée approximative</th>
			<th scope="col">Date</th>
		</tr>
	</thead>
	<tbody>
		<tr>
			<td><img src="/img/icon_call_missed.png?v=3.4" alt="Appel manqué" title="Appel manqué" /></td>
			<td>Voip/td>
			<td>06XXXXXXXX</td>
			<td><b>Appel manqué</b></td>
			<td>Vendredi 12 Juin 2020 à 18h15</td>
		</tr>

...

Pour automatiser cela, j’écris une fonction qui extrait du code source reçu le texte situé entre les chaînes de caractères :

title=\"Appel manqué\" />

et :

</tr>

Je récupère ainsi la sous-chaîne suivante pour chaque appel en absence qui apparaît dans l’historique du téléphone :

</td>
<td>Voip/td>
<td>06XXXXXXXX</td>
<td><b>Appel manqué</b></td>
<td>Vendredi 12 Juin 2020 à 18h15</td>

Toutefois, cette chaîne de caractères doit encore être nettoyée car elle contient des balises HTML résiduelles, ainsi que des espaces, nouvelles lignes et tabulations. Je remplace donc dans un premier temps les nouvelles lignes par des espaces puis j’utilise une expression régulière pour extraire les informations utiles :

(0[0-9]+ X+).+  ([a-zA-Z0-9à ]+[0-9])

Avant de terminer, il y a un point très important à ne pas omettre : se déconnecter de l’interface Web de la box ! En effet, la box semble conserver en mémoire une centaine de sessions en parallèle, mais au-delà, la box refuse les nouvelles authentifications. Il suffit pour cela d’appeler la page http://192.168.1.1/logout :

curl -L -c /tmp/cookie.txt -b /tmp/cookie.txt http://192.168.1.1/logout

Pour détecter un nouvel appel en absence, le serveur se connecte régulièrement à la box et récupère la date et l’heure du dernier appel manqué. Celles-ci sont comparées à celles obtenues lors du dernier tour de boucle. Si elles ont changé, c’est qu’un nouvel appel a été reçu. Je m’envoie alors un SMS grâce à ma passerelle Web <-> SMS que je décrirai un peu plus tard.

Et voilà ! Je ne sais pas si cela me sera vraiment très utile au quotidien, mais en tout cas je me serai bien amusé 🙂

Comme le code source n’est pas bien long, je le met ici au cas où celui-ci serve à quelqu’un :

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <regex.h>
#include <time.h>

#include "main.h"


// Prototypes fonctions privées
char *extract_between(char *str, char *p1, char *p2, char **str_suivant);
void recup_dernier_appel_manque(char *numero_tel, size_t taille_tel, char *date_heure, size_t taille_dh);


// Variables
char dernier_numero_tel[25] = "", derniere_date_heure[100] = "";
time_t ts_derniere_requete = 0;


/////////////////////////////////////////////////////////////////
/////////////// Fonctions publiques
/////////////////////////////////////////////////////////////////

// Fonction appelée à l'initialisation
void telephone_fixe_init()
{
	// Au démarrage, on cherche le dernier appel manqué
	recup_dernier_appel_manque(dernier_numero_tel, sizeof(dernier_numero_tel), derniere_date_heure, sizeof(derniere_date_heure));
	ts_derniere_requete = time(NULL);
	
	printf("Dernier appel en absence : le %s du %s\n", derniere_date_heure, dernier_numero_tel);
}



// Fonction appelée en boucle
void telephone_fixe_loop()
{
	char numero_tel[25] = "", date_heure[100] = "";
	
	// Il est temps de regarder si un appel a été manqué
	if(time(NULL) - ts_derniere_requete > INTERVALLE_RECHERCHE_APPEL_MANQUE)
	{
		recup_dernier_appel_manque(numero_tel, sizeof(numero_tel), date_heure, sizeof(date_heure));
		
		
		//printf("Dernier appel en absence : le %s du %s\n", date_heure, numero_tel);
		if(strcmp(date_heure, derniere_date_heure) != 0) // Un nouvel appel en absence est détecté
		{
			char tmp[500];
			snprintf(tmp, sizeof(tmp), "Evénement: appel en absence sur le téléphone fixe du %s le %s !", numero_tel, date_heure);
			sms(NUMERO_TEL_DAMIEN, tmp);
			//sms(NUMERO_TEL_FANNY, tmp);
			
			strcpy(dernier_numero_tel, numero_tel);
			strcpy(derniere_date_heure, date_heure);
		}
		
		ts_derniere_requete = time(NULL);
	}
	
	
}

/////////////////////////////////////////////////////////////////
/////////////// Fonctions privées
/////////////////////////////////////////////////////////////////



// Récupère le numéro de téléphone et la date du dernier appel manqué
void recup_dernier_appel_manque(char *numero_tel, size_t taille_tel, char *date_heure, size_t taille_dh)
{
	// On soumet la page de connexion et on récupère le cookie
	system("/usr/bin/curl -s -L -c /tmp/cookie.txt -d 'method=passwd&page_ref=/voip/config&zsid=&hash=&login=[identifiant]&password=[mot_de_passe]&submit_button=' -X POST http://192.168.1.1/login > /dev/null");
	
	// On récupère le code HTML contenant les appels en absence dans "buffer"
	FILE* file = popen("/usr/bin/curl -s -L -c /tmp/cookie.txt -b /tmp/cookie.txt http://192.168.1.1/voip/config", "r");
    char buffer[25000];
	size_t taille;
	
    taille = fread(buffer, 1, sizeof(buffer) - 1, file);
	buffer[taille] = '\0';
    pclose(file);
    //printf("buffer is :%s\n", buffer);
	
	
	// On extrait les appels en absence du code HTML
	char *buffer_incremente = buffer;
	char *chaine_extraite;
	while((chaine_extraite = extract_between(buffer_incremente, "title=\"Appel manqué\" />", "</tr>", &buffer_incremente)) != NULL)
	{
		// Ici, on a donc une "chaine_extraite" par appel en absence, mais elle est encore grossière (contient encore du HTML) et doit être mise en forme
		
		// On remplace les retours à la ligne par des espaces
		char *p;
		for(p = chaine_extraite; *p != '\0'; p++)
		{
			if(*p == '\n')
				*p = ' ';
		}
		
		//printf("Chaîne extraite : %s\n\n\n", chaine_extraite);
		
		
		
		// On extrait le numéro de téléphone et la date avec une regex
		regex_t regex;
		int status;
		regmatch_t pmatch[3];

		if (regcomp(&regex, "(0[0-9]+ X+).+  ([a-zA-Z0-9à ]+[0-9])", REG_EXTENDED) != 0)
			printf("regcomp error\n");
		
		status = regexec(&regex, chaine_extraite, 3, pmatch, 0);
		regfree(&regex);
		if (!status)
		{
			
			if(pmatch[1].rm_so != -1) // On a trouvé le numéro de téléphone
			{
				int taille = pmatch[1].rm_eo - pmatch[1].rm_so;
				if(taille > taille_tel - 1)
					taille = taille_tel - 1;
				
				memcpy(numero_tel, chaine_extraite + pmatch[1].rm_so, taille);
				numero_tel[taille] = '\0';
			}
			
			if(pmatch[2].rm_so != -1) // On a trouvé la date et l'heure
			{
				int taille = pmatch[2].rm_eo - pmatch[2].rm_so;
				if(taille > taille_dh - 1)
					taille = taille_dh - 1;
				
				memcpy(date_heure, chaine_extraite + pmatch[2].rm_so, taille);
				date_heure[taille] = '\0';
			}
			
			// On a trouvé !
			//printf("trouvé ! Tel = '%s', date-heure = '%s' \n\n\n", numero_tel, date_heure);
		}
		
		free(chaine_extraite);
	}
	
	// On se déconnecte de l'interface de la box
	system("/usr/bin/curl -s -L -c /tmp/cookie.txt -b /tmp/cookie.txt http://192.168.1.1/logout > /dev/null");
}



// Renvoie un pointeur vers un tableau alloué dynamiquement dans lequel est copié la sous-chaîne de "str" située entre "p1" et "p2"
// "str_suivant" est modifié pour contenir l'adresse de str
char *extract_between(char *str, char *p1, char *p2, char **str_suivant)
{
	char *i1 = strstr(str, p1); // récupération du premier index
	if(i1 != NULL) // il existe
	{
		size_t pl1 = strlen(p1);
		char *i2 = strstr(i1 + pl1, p2); // récupération du deuxième index
		if(p2 != NULL) // il existe
		{
			// Found both markers, extract text
			size_t mlen = i2 - (i1 + pl1);
			char *ret = malloc(mlen + 1);
			if(ret != NULL)
			{
				memcpy(ret, i1 + pl1, mlen);
				ret[mlen] = '\0';
				
				// on stocke le point de départ de la chaîne suivante
				if(str_suivant != NULL)
					*str_suivant = i2;
				
				return ret;
			}
		}
	}
	
	return NULL;
}


Un commentaire

  1. > Dans mon cas, je dois donc me contenter de la première solution, même si cela implique d’inscrire le clair sur le serveur les identifiants de connexion (si vous avez une idée pour faire autrement n’hésitez pas à lâcher vos com’s).

    Mets un servo sur le bouton wps? 🤣 Ou un transistor :p

    Sinon, si l’authentification persiste suffisamment longtemps, tu peux peut-être juste te servir du cookie?

    Je note qu’il suffit que je sniffe ton réseau local pour récupérer le mot de passe, peut-être que 192.168.1.1 fonctionne en https même si c’est un certificat auto-signé?

    Sinon, je crois que de mon côté, je vais laisser mon serveur chez mes parents, et passer à un FAI local et associatif: illyse (https://www.illyse.net/, plus d’infos sur les FAI associatifs sur https://www.ffdn.org/fr/principes-fondateurs). Ils n’ont pas de voip, mais m’ont conseillé https://www.ovhtelecom.fr/telephonie/voip/ qui est à 1€ HT/mois, numéro actuel portable.

    Et pour la dernière pièce de l’offre Triple Play, un décodeur satellite PCIe ou USB branché sur mon serveur, avec jellyfin, tvheadend, et/ou kodi 🙂

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *