TD 4 INF422 — Programmation shell

Introduction      FAQ   Glossary   Project   TD 1   TD 2   TD 3   TD 4   TD 5   TD 6   TD 7   TD 8

Retour au cours.

Objectifs :

Pré-requis :

0. Description d'un shell

Dans cette partie nous vous proposons une référence pour les fonctionnalités de base d'un shell UNIX, comme celui d'Android. Un shell -- ou interpréteur de commandes -- est un programme qui permet de lancer (facilement) d'autres programmes. Les plus connus sont /bin/bash, /bin/zsh, /bin/ksh, /bin/tcsh, /bin/ash (celui d'Android), ou pour le plus simple (Bourne) /bin/sh.

Rappel, pour se connecter au shell d'Android :

  emulator @inf422 -ramdisk <IMAGES>/ramdisk.img -kernel <IMAGES>/kernel-qemu
  adb forward tcp:4444 tcp:23
  telnet localhost 4444
Le login est root.

0.1 Les commandes de base

Voici un premier jeu de commandes pour naviguer dans le système de fichiers d'UNIX (cliquer pour avoir une page de manuel Linux) :

Quelques commandes pour manipuler les fichiers :

Quelques commandes pour manipuler les processus :

Tout programme dispose d'une entrée standard (stdin), d'une sortie standard (stdout) et d'une sortie d'erreur (stderr). Lorsqu'il est exécuté dans un shell, les caractères tapés au clavier sont envoyés sur l'entrée standard et la sortie standard est ce qui s'affiche à l'écran sur la console.

Dans le cas de la plupart des programmes UNIX ci-dessus, quand un nom de fichier n'est pas fourni en argument, les données sont alors lues depuis stdin. Il est possible de rediriger l'entrée et les sorties, soit vers des fichiers, soit vers d'autres commandes :

Un shell est un programme qui s'exécute sur la machine, et qui founit un environnement pour exécuter des commandes. On peut y définir des variables d'environnement. Ces variables sont accessibles par tout programme lancé depuis ce shell. La plus connue est la variable PATH qui contient la liste des répertoires dans laquelle le shell va rechercher les programmes nécessaires à l'exécution de la commande saisie.

Par exemple, vous pouvez taper directement ls, alors que le chemin complet de la commande est en fait /bin/ls. Ceci est possible car la variable PATH contient /bin.

Quelques commandes utiles pour manipuler les variables d'environnements :

Pour finir, un shell moderne propose plusieurs fonctionnalités pour faciliter la saisie d'une commande :

Pour en savoir plus :

0.2 Fichier de script shell

Le shell étant un programme comme un autre, il est capable de lire une série de commandes depuis un fichier. Si vous tapez une liste de commandes dans un fichier (par exemple script.sh), elles seront exécutées comme si vous les aviez saisies une par une. Voici un exemple d'exécution de plusieurs commandes entrées intéractivement dans le shell (la sortie standard est représentée en vert), en utilisant le shell standard /bin/sh :

# pwd
/home
# cat script.sh
echo "liste des fichiers :"
ls
# sh < script.sh
liste des fichiers :
tmp
script.sh
# sh script.sh
liste des fichiers :
tmp
script.sh

Le fichier script.sh est appelé un shell-script. Toute commande qui peut être saisie dans le shell peut être contenue dans un shell-script. Cela inclut les commandes complexes (cmd1 | cmd2) et les structures de contrôle (if, while, ...).

Voici un exemple de shell-script, example.sh, qui affiche le nom et l'heure du système, et optionnellement le nom de la machine :

#!/bin/sh

## Affiche le nom complet du systeme.
uname
## Affiche l'heure.
date
## Memorise le nom de la machine dans une variable.
hostname=`uname -n`
## On initialise la variable default_name.
default_name="localhost"
## Si le nom de la machine n'est pas "localhost", on l'affiche.
if [ "$hostname" != "$default_name" ]; then
    echo "$hostname";
fi

Remarque 1 Notez la première ligne, #!/bin/sh, qui précise que le fichier est un shell-script à interpréter avec ce shell, ce qui permet d'exécuter directement la commande ./fichier.sh (le préfixe ./ est nécessaire si PATH ne contient pas "."), au lieu de sh fichier.sh. Attention vous devez donner les droits en exécution à votre script fichier.sh avec la commande chmod a+x fichier.sh pour pouvoir l'exécuter directement.

Remarque 2 Sur la troisème ligne (hostname=`uname -n`), nous avons utilisé une construction particulière du shell, très pratique. Au lieu de stocker le résultat d'une commande (stdout) dans un fichier, on peut le stocker dans une variable (ici hostname), en placant cette commande entre deux ` (backquote).

Pour créer et modifier des fichiers, plusieurs programmes d'édition de texte sont disponibles :

Bien que nous vous recommandions fortement d'apprendre à manipuler vi à cause de sa présence quasi certaine sur tout système UNIX que vous rencontrerez, nous vous conseillons pour le TD de vous orienter vers ee.

Pour en savoir plus :

A titre d'exemple et de référence, vous pouvez lire le shell-script suivant, web_server.sh, qui émule un serveur web très simple (que vous pouvez lancer avec nc -l -l -p 80 -e ./web_server.sh).

#! /bin/sh

# Definit le repertoire contenant le site web.
base=/home/www
# Lit une ligne sur l'entree standard, la requete
read request
# Lit le header envoye par le navigateur web.
while true; do
  # Lit une ligne.
  read header
  # Si cette ligne est un retour chariot (CR), arreter la lecture.
  if [ "$header" == $'\r' ]; then
    break
  fi
done
# Une requete est de la forme 
# GET /index.html HTTP/1.1
# Decouper la requete pour recuperer /index.hml
urlfile=`echo "$request" | sed -r "s/.*GET[ ]+[\/]?(.*)[ ]+HTTP.*/\1/g"`
# Si l'url est vide, par defaut on renvoie index.html.
if [ -z "$urlfile" ]; then
   urlfile="index.html"
fi
# Le nom de fichier desire : par exemple /var/www/index.html
filename="$base/$urlfile"
# Tester l'existence du fichier.
if [ -f "$filename" ]; then
  # Il existe.
  # Ecrire sur la sortie standard le header de reponse.
  echo -e "HTTP/1.1 200 OK\r"
  echo -e "Content-Type: text/html\r"
  echo -e "\r"
  # Ecrire le fichier sur la sortie standard.
  cat "$filename"
  echo -e "\r"
else
  # Il n'existe pas.
  # Ecrire sur la sortie standard le header de reponse.
  echo -e "HTTP/1.1 404 Not Found\r"
  echo -e "Content-Type: text/html\r"
  echo -e "\r"
  echo -e "404 Not Found\r"
  echo -e "Not Found
           The requested resource was not found\r"
  echo -e "\r"
fi

1 Prise en main du shell

1.1 Les commandes de base

Vous allez vous familiariser avec le shell d'Android. la description des commandes disponibles ainsi que leurs options est disponible ici.

La plupart des commandes UNIX standards sont disponibles, mais d'un système à un autre il se peut que certaines options n'existent pas ou aient une syntaxe légèrement différente. Ici en cas de problème réferrez-vous à la documentation des commandes disponibles dans le shell d'Android.

Familiarisez-vous avec les commandes pour :

1.2 Un premier script

2. Un serveur de message

Dans cet exercice nous allons découvrir comment écrire un programme qui est appelé automatiquement par le système lorsqu'une connexion réseau est initiée sur un certain port.

Nous allons réaliser un programme de réponse automatique en cas de vacances de l'utilisateur. Lorsqu'une connexion sera effectuée sur le port 5000, notre programme renverra la chaîne xxx est en vacances.

2.0 Le super-démon inetd

Bien sur, il serait possible de créer un programme qui s'execute en permanence et qui écoute sur le port 5000, afin de renvoyer le contenu du fichier .vacation. Le but ici est de :

Il y a donc deux parties à réaliser :

  1. écrire le programme d'envoi du message,
  2. configurer le système pour exécuter ce programme lors d'une connexion sur le port 5000.

2.1 Ecriture du programme

On peut rediriger l'entrée et la sortie standard d'un programme. Ici, nous confierons au programme inetd le soin de rediriger automatiquement stdin et stdout sur la connexion réseau initiée sur le port 5000. Pour écrire sur cette connexion réseau (c-a-d cette socket), le programme doit simplement écrire sur stdout, ce qui est déjà le comportement par défaut. De même, pour lire des informations sur cette socket, il suffit de lire sur stdin. Ainsi pour tester votre programme vous pouvez le lancer sur votre machine, en simulant ce qui est reçu par le réseau en tapant au clavier, et en contrôlant ce qui y est envoyé en regardant ce qui s'affiche sur la console.

Ecrivez un script /home/bin/vacation.sh qui écrit sur la sortie standard le contenu du fichier /home/.vacation, que vous aurez créé au préalable et qui contiendra le message à envoyer.

2.2 Configuration du système

Le super-démon inetd (internet "super-server") a pour fonction d'associer un service à un programme à exécuter en cas de connexion à ce service. Un service associe un nom (un identifiant) à un ou plusieurs ports réseaux, et est spécifié dans le fichier /etc/services. Voici un extrait de ce fichier :

http            80/tcp
http            80/udp
https           443/tcp
https           443/udp

Le service http est associé au port 80 en mode TCP, et également en UDP (les deux types de connexion réseau utilisées habituellement). Similairement, le service https (HTTP sécurisé) est associé au port 443.

Le programme inetd prend en argument un fichier de configuration, contenant la liste des services pour lesquels on souhaite exécuter un programme particulier en cas de rêquete à ce service. Voici un exemple de fichier de configuration, inetd.conf, pour inetd :

http    stream  tcp nowait  root    /home/bin/webserver.sh

Ici nous associons au service http la commande /home/bin/webserver.sh. stream indique le type de socket, tcp est le protocole utilisé, nowait indique que le multiplexage est possible (un nouceau client peut être accepté alors que d'autres sont encore entrain d'être servis), root indique quel est l'utilisateur à utiliser pour lancer le programme, et /home/bin/webserver.sh le nom complet du programme à exécuter. Pour plus de détails, vous pouvez consulter le manuel ici. Pour le reste du TD, nous utiliserons toujours stream tcp nowait root.

Pour résumer, voici la liste des tâches pour configurer son système :

  1. Optionnellement vous pouvez créer une entrée dans /etc/services associant un nouveau nom de service à un port (attention a ne pas prendre un port déja utilisé), par exemple :

    my_test_service		5000/tcp
    my_test_service		5000/udp
    

    Dans la version d'Android dont vous disposez, le fichier /etc/services n'est pas persistant : si vous éteignez l'émulateur, vos modifications seront effacées. Vous devrez donc refaire vos modifications dans /etc/services à chaque fois que vous lancerez l'émulateur. Pour éviter ce problème nous vous conseillons de saisir directement un numéro de port dans /home/inetd.conf.

  2. Créer ou modifier le fichier de configuration /home/inetd.conf pour associer le service my_test_service ou directement le numéro de port au programme à exécuter :

    5000	stream	tcp	nowait	root	/home/bin/my_test_demon.sh
    
  3. Lancer depuis le shell le programme inetd avec le fichier de configuration (on commence par tuer tout autre inetd qui s'exécuterait sur la machine, pour éviter les conflits) :

    killall inetd
    inetd /home/inetd.conf
    

Nous allons maintenant paramétrer le système pour qu'il exécute notre programme lors de toute connexion entrante sur le port 5000, grâce à inetd.

Configurer votre système pour qu'il exécute le programme /home/bin/vacation.sh lors d'une connexion sur le port 5000.

Quelques conseils.

Pour tester votre application entre plusieurs utilisateurs, vous devez pouvoir vous connecter à votre émulateur depuis Internet. Cette fonctionnalité n'est pas disponible, car Android n'est pas conçu pour permettre cette connexion Internet > émulateur, seul émulateur > Internet est possible par défaut. Nous vous proposons une application Java standard contournant cette difficulté :

Pour utiliser cette application : Cette application crée un pont entre votre adresse IP internet, sur la liste de ports sélectionnée dans l'application, vers l'adresse 127.0.0.1 (le loopback, reconnu par Android) sur ces même ports.

  • Vous devez télécharger et utiliser l'application LoopbackPortForwarding pour permettre à Android d'être accessible sur le réseau. Réglez l'application sur le port 5000
  • Vous devez permettre au port 5000 de l'émulateur d'Android d'être accessible, grâce à la commande

    adb forward tcp:5000 tcp:5000
  • Pour tester votre programme entre vous, faites nc ip_du_voisin 5000
  • 3. Contrôle du système à distance

    Dans cet exercice nous allons écrire un programme qui peut gérer à distance les processus du système.

    3.1 Ecriture du programme

    Ecrivez un script /home/bin/backdoor.sh qui effectue les taches suivantes :

    Correction: fichier /home/bin/backdoor.sh.

    #!/bin/sh
    read command
    $command
    

    3.2 Configuration du système

    Configurer votre système pour qu'il exécute le programme /home/bin/backdoor.sh lors d'une connexion sur le port 6666.

    Quelques conseils :

    Correction: vérifiez qu'inetd ne tourne pas (ps, killall inetd) puis lancez inetd /home/inetd.conf en ayant préalablement créé le fichier /home/inetd.conf suivant.

    6666	stream	tcp	nowait	root	/home/bin/backdoor.sh
    

    3.3 Amélioration du programme

    Notre programme peut exécuter n'importe quelle commande sur le système, ce qui représente un faille de sécurité très importante. On préfèrera permettre l'accès à certaines commandes uniquement.

    Modifiez le script /home/bin/backdoor.sh pour qu'il effectue les taches suivantes :

    Correction: fichier /home/bin/backdoor.sh.

    #!/bin/sh
    read command
    
    arg0=`echo "$command" | cut -d\  -f 1`
    arg1=`echo "$command" | cut -d\  -f 2`
    
    if [ "$arg0" = kill ]; then
      kill $arg1
    elif [ "$arg0" = list ]; then
      ps
    else
      $command
    fi
    

    4. Exécution de commande UNIX depuis une application Android

    Dans cet exercice nous allons écrire une application Android en Java (comme lors des TD précédents), qui va permettre de saisir une commande UNIX sur le téléphone, et qui affichera le résultat de cette commande sur le téléphone.

    Note : l'exercice 3.1 et 3.2 de ce TD doit être fini et le programme backdoor.sh doit fonctionner comme prérequis à cet exercice.

    Créez un nouveau projet Android dans Eclipse, nommé ShellCommand.

    4.1 Spécification de l'interface graphique

    Spécifiez l'interface graphique dans le fichier res/layout/main.xml. Votre interface doit contenir :

    • Un élément de type EditText nommé command pour contenir la commande
    • Un élément de type Button nommé execute qui exécutera la commande command quand on clique dessus
    • Un élément de type EditText nommé result pour contenir le résultat affiché par cette commande. Vous pouvez paramètrer la taille d'un objet EditText, par exemple avec les attributs :

      android:layout_width="250px"
      android:layout_height="250px"
      

    4.2 Connexion au programme d'exécution de commande

    Créez le programme qui :

    • Crée une connexion (Socket) sur le port 6666 de localhost (adresse IP 127.0.0.1). backdoor.sh doit être activé comme proposé dans l'exercice 3.
    • Envoie le contenu de l'EditText command sur cette socket lorsque l'on clique sur le bouton execute.
    • Lit sur cette socket toutes les lignes renvoyées, et les affiche sur l'EditText result.

    Quelques conseils :
    • Réferrez vous aux TD précédents pour retrouver tous les éléments nécessaires à la réalisation de cet exercice.
    • En particulier, puisque votre programme va utiliser le réseau, vérifiez que les permission nécessaires sont attribuées dans AndroidManifest.xml.

    5 Exercice bonus (optionnel)

    Vous allez réaliser deux scripts, pour gérer la sauvegarde des informations de configuration du système. Lorsque l'on manipule les fichiers de configuration, il est en effet essentiel de toujours sauvegarder ces fichiers avant de les modifier, en cas de mauvaise manipulation...

    Vous devez sauvegarder :

    • Créer le shell-script /home/bin/backup_config.sh qui sauvegarde les fichiers dans le répertoire /home/backup (que vous devez créer au préalable)
    • Ne pas oublier de donner les droits en exécution à votre script avec la commande chmod a+x backup_config.sh
    • Vous pourrez utiliser les commandes dirname et basename.

    • Créer le shell-script /home/bin/restore_config.sh qui copie les fichiers du répertoire /home/backup vers leur place originale

    Améliorations possibles :