Bash : Géolocalisation, la trilogie
Trois itérations du même script, entre décembre 2008 et octobre 2009, chacune née de la précédente et de ses limites. La v1 est perdue avec le disque dur (un Samsung de 500 Go, avec tous mes projets Bash, C, PHP et Perl, ma musique perso, bref le truc plein à plus de 90%), la v2 et la v3 survivent. C’est l’histoire de la progression d’un script en public.
La revanche de netcat (v2, février 2009)
Après une discussion IRC sur la géolocalisation, un ami qui utilisait le même site m’a refilé un de ses scripts qui en une seule ligne faisait exactement la même chose que le mien mais beaucoup plus rapidement grâce à netcat (pas d’accès disque). Malheureusement, suite à la perte de mon disque dur, j’ai perdu pas mal d’outils, dont ce fameux script. Du coup j’ai réécrit ce script, étant quand même largement moins bon que mon ami, il me prend plus d’une ligne.
Le script geolocv2.sh interroge geoiptool.com avec netcat, en construisant les en-têtes HTTP à la main:
#!/bin/bash
BIN="/bin/nc"; BIN1="/usr/bin/awk"; BIN2="/usr/bin/cut";
HOST="www.geoiptool.com"; PORT="80"; UA="GeoIP(0.02)";
HEAD="GET /fr/?IP=$1";
H="Host:$HOST\r\nUser-Agent:$UA\r\nAccept:text/html\r\nAccept-Language:fr,fr-fr\r\nAccept-Charset:ISO-8859-1,utf-8\r\nConnection:close\r\n\r\n";
affiche(){
page=$(echo -ne "$HEAD${H}" | $BIN $HOST $PORT);
echo "FQDN : $(echo $page | $BIN1 'BEGIN {FS = "arial_bold"}{print $3}' | $BIN2 -d '>' -f2 | $BIN2 -d '<' -f1)";
echo "IPv4 : $(echo $page | $BIN1 'BEGIN {FS = "arial_bold"}{print $4}' | $BIN2 -d '>' -f2 | $BIN2 -d '<' -f1)";
echo "Pays : $(echo $page | $BIN1 'BEGIN {FS = "arial_bold"}{print $5}' | $BIN2 -d '>' -f2 | $BIN2 -d '<' -f1)";
echo "Ville : $(echo $page | $BIN1 'BEGIN {FS = "arial_bold"}{print $8}' | $BIN2 -d '>' -f2 | $BIN2 -d '<' -f1)";
}
affiche
Le point technique: netcat ouvre la connexion, envoie les en-têtes HTTP à la main (le GET /fr/?IP=$1 avec les headers construits en tableau) et lit la réponse HTML de geoiptool.com. Le parsing se fait en awk avec un FS sur arial_bold (le nom de la classe CSS du site) et des cut sur les chevrons. C’est du HTML parsing à la force brute et ça marche parce que le site a un HTML simple et stable.
La sortie:
FQDN : vhost.crawl.co.jp
IPv4 : 59.106.153.9
Pays : JP (JPN)
Ville : Osaka
La revanche de bash (v3, octobre 2009)
La troisième itération supprime netcat: bash a un /dev/tcp intégré depuis le bash 2.04 et ce pseudo-device permet d’ouvrir une connexion TCP sans rien installer. Plus de netcat, plus d’awk externe obligatoire (mais awk reste plus lisible que le bash pur pour le parsing) et le script tient dans un heredoc tapé d’un seul bloc:
#!/bin/bash
if [[ -z $1 ]];then
echo "Usage : geoloc <ip/host>"
else
BIN1="/usr/bin/awk"; BIN2="/usr/bin/cut";
page=$(exec 3<>/dev/tcp/www.geoiptool.com/80;
echo -e "GET /?IP=$1 HTTP/1.1\r\nHost:www.geoiptool.com\r\n\r\n" >&3;
cat <&3);
echo "FQDN : $(echo $page|$BIN1 'BEGIN {FS = "arial_bold"}{print $3}'|$BIN2 -d '>' -f2|$BIN2 -d '<' -f1)";
echo "IPv4 : $(echo $page|$BIN1 'BEGIN {FS = "arial_bold"}{print $4}'|$BIN2 -d '>' -f2|$BIN2 -d '<' -f1)";
echo "PAYS : $(echo $page|$BIN1 'BEGIN {FS = "arial_bold"}{print $5}'|$BIN2 -d '>' -f2|$BIN2 -d '<' -f1)";
echo "REGION : $(echo $page|$BIN1 'BEGIN {FS = "arial_bold"}{print $7}'|$BIN2 -d '>' -f3|$BIN2 -d '<' -f1)";
echo "VILLE : $(echo $page|$BIN1 'BEGIN {FS = "arial_bold"}{print $8}'|$BIN2 -d '>' -f2|$BIN2 -d '<' -f1)";
unset page
fi
Le /dev/tcp/host/port n’est pas un fichier: c’est un builtin bash qui ouvre une connexion TCP quand on le redirige. Le exec 3<> ouvre le fd 3 en lecture-écriture, le echo >&3 envoie la requête HTTP, le cat <&3 lit la réponse. Le tout dans une subshell ($(...)), sans subprocess externe pour le transport: bash fait le TCP nativement.
La sortie, avec le vrai test d’époque:
$ ./geolocv3.sh www.toto.com
FQDN : vhost.crawl.co.jp
IPv4 : 59.106.153.9
PAYS : JP (JPN)
REGION : Osaka
VILLE : Osaka
Ce que la trilogie raconte
La v1 (perdue) utilisait netcat et un accès disque. La v2 garde netcat mais supprime l’accès disque (le script de l’ami en une ligne vs le mien en dix). La v3 supprime netcat et remplace le transport par le builtin bash. Chaque itération a supprimé une dépendance: d’abord le fichier temporaire, puis le netcat.
Le /dev/tcp est le genre de builtin que personne n’utilise: il est documenté dans man bash à la section REDIRECTION, il est désactivé dans certains shells de production (le set -o restricted le bloque) et il fait exactement la même chose qu’un netcat sans installer rien. C’est la leçon de la v3: avant d’installer un outil, vérifier que le shell ne sait pas déjà le faire.
Un aveu pour finir. La version qui a réellement survécu dans mes .bash_functions n’était aucune des deux, c’était la plus bête: un wget -qO- http://www.geoiptool.com/?IP=$1 | html2text | grep 'Host Name\|IP Address\|City\|Country\|code\|itude', cinq pipes et zéro finesse. Toute cette trilogie à parser du HTML à la main n’était qu’un prétexte à jouer avec netcat et /dev/tcp et le parsing par classe CSS cassait au premier relooking du site. Aujourd’hui la question ne se pose même plus: curl ipinfo.io/8.8.8.8 rend du JSON propre et c’est tout ce qu’il reste de ces dix lignes.