dimanche 7 octobre 2018

Déployer un war dans tomcat sur openshift

Pour déployer un war tomcat sur openshfit le moyen le plus simple que j'ai trouvé est d'utiliser l'image S2I (Source-to-Image) de Sarcouy. Vous verrez dans l'exemple que je présente qu'il n'est pas nécessaire d'utiliser un repository git.

On va donc procéder avec les étapes suivantes :

  1. Créer un projet openshift my-experiences
  2. Creer un projet maven my-app pour faire un war, mais cela peut être votre code existant.
  3. Créer un imagestream tomcat-builder qui embarque le builder de Sarcouy
  4. Creer un imagestream my-tomcat-app qui va recevoir l'image produite par le builder
  5. Creer le builder 
  6. Lancer un build qui va alimenter l'imagestream my-tomcat-app
  7. Créer une application à partir de l'imagestream my-tomcat-app


Creer un projet maven my-app pour faire un war


oc new-project my-experiences
mvn archetype:generate \ 
   -DarchetypeGroupId=org.apache.maven.archetypes \
   -DarchetypeArtifactId=maven-archetype-webapp \
   -DarchetypeVersion=1.3 \
   -DgroupId=fr.maker.paas \
   -DartifactId=my-app \
   -Dversion=1.0-SNAPSHOT 
# verifier que ça construit bien 
cd my-app && mvn package

On crée les imagestream necessaires

oc create imagestream tomcat-builder
oc import-image --from=sarcouy/s2i-tomcat:8.5-jdk8-mvn3.3.9 tomcat-builder
oc create imagestream my-tomcat-app


On crée le builder

On utilisera le paramètre --binary qui nous permet de ne pas utiliser un repo mais de donner en paramètre de lancement le répertoire du code source

#ceci va créer le buidconfig my-tomcat-app qui 
#par défaut récupère le nom de l'imagestream de destination
oc new-build --image-stream=tomcat-builder --binary --to=my-tomcat-app
# le builder de sarcouy nous impose de préciser le nom du war a déployer 
# en variable d'environnement
# je suppose qu'il a du tomber sur des projets qui fabriquaient plusieurs war
# on patch donc le buildconfig 
# vous pourvez aussi utiliser aussi oc edit
oc patch bc my-tomcat-app -p \
  '{ "spec" :{"strategy":{"sourceStrategy":{ "env" : [{"name":"WAR_NAME", "value": "my-app.war"}] } } } }'


On lance le build qui va créer l'image et la mettre dans l'imagestream de destination

oc start-build my-tomcat-app --from-dir=/vagrant/lab/05_builder/tomcat/my-app/ --follow

Ceci construit et pousse l'image dans la registry interne d'openshift, et toute image dans la registry interne est aussi une imagestream. Donc l'image est ici le tag latest de l'imagestream my-tomcat-app.

On déploie
oc new-app --image-stream=my-tomcat-app
#on expose pour la rendre accessible depuis internet
oc expose svc/my-tomcat-app

A chaque fois qu'on relancera un build le deploymentconfig qui référence cet imagestream détectera le changement et redéploiera automatiquement l'image.

mardi 15 novembre 2016

Understanding kerberos

Kerberos allow different nodes of a network to authenticate to each other even if this network is not secure. Not secure means that eavesdroppers are listening on the network and may catch your data to fake your identity.

I didn't find easy to understand this protocol and why it was designed in such way, so I decided to build my own tutorial.

My main source of inspiration was this two links : microsoft technet  and the RFC 

Let's start with two nodes that want to authenticate to each-over.

A naive solution would be sharing a secret password
A emit an authent request to B accompagnied with password that A and B share. 
B check the password and authenticate A. 

The problem is eavesdroppers. They're listening on the network, if they catch the password they can build the authent request for them and fake the identity.

 
Is there a solution to this problem ? The answer is yes : Symetric Encryption ! Symetric encryption is the process of encoding messages with a key (a sequence of bytes) in such a way that you can only decode it if you also have this key.

 A and B share the same secret key that they never send in clear over the network. Let's call this key Kab.  

We could build the message to authenticate this way : Authent Request + A + Encypt(Kab, Timestamp)



When B receive an authent request from A he uses the key Kab to decode the timestamp, if  he finds a valid timestamp he authenticates A. The timestamp is used to prevent an eavedropper to just resend the whole packet to B, because B will accept the authent request only if the timestamp is recent enough. We could even imagine that B will never accept twice the same timestamp from A.

With this process an eavedropper is unable to build a valid authent request.

Mutual authentication is even feasible with B resending the timestamp sent previously by A.

But it could be a problem because the eavedropper have now the timestamp and its encrypted version so it can start to search a key by brute forcing. Thus some random data are added before encryption.

B was able to split timestamp and random data, it's a proof for A that B has also the key Kab.

How this solution scale ? 

But this solution does not scale well. Each node have to share a key for each relation. For instance in a network with A, B, C, and D the node A will need to maintain 4 keys Kab, Kac and Kad to manage all the authentications with the other nodes and the node B will have to maintain Kab, Kbc and Kbd, and so on for C and D. 


An other problem occur when you need to add or remove a node in the network. The new node has to notify all the other nodes in order to share with them a key, it may involve a lot of exchange between node that potentially will never need to communicate anyway. 

And what happen if one of them is inactive or unvailable, when the new node is arriving in the network, they'll have to synchronize lately ? 

The  kerberos protocol

To solve this problem the kerberos protocol propose a solution. Each node share its key with a special node called the KDC (Key Distribution center). All the node sharing a key with the KDC make a group called realm.


So what happen when A want to authenticate with D for instance ?



(1) A ask the KDC he wants to athenticate with D
(2) KDC return a message containing a Kad key (called session key) encrypted with the Ka key (called the long term key) so that only A can decrypt it. Along the message there's also the Kad, with the requester and a validity timstamp encrypted with Kd. A cannot decrypt this message as he doesn't know the Kd key. Thus he'll only be able to resend it. This part is called the kerberos ticket.
(3) A send its authent request, with the kerberos ticket and a timestamp encrypted wit Kad if A want mutual autehntication.
(4) D use its Kd key to find Kad, the requester (A) and the validity of the request. If the requester and the validity match then the authentication is accepted. Now A and D share the same Kad key. D may use it to decrypt the timestamp  and resent it to A for mutual authentication.

A big difference wih other protocols like NTLM for instance deserve to be noted here : D don't need to connect to the KDC. The authentication of A could be validated with the information provided by the client.

The ticket granting ticket (TGT)

Actually the nodes in the system are separated between two populations : 
  1. The services. The services is what's behind a server software like print server, web server, shared file system and so on. 
  2. The users. The users is usually human being constantly connecting and disconnecting the network.
For the services the easiest way to share the keys is to copy the key between the KDC and the service. As the services are numerically much fewer than users and  stay on the network all the time it's fine. Beside we control what's installed on the servers which is not the case with the users. 

For the users the key is derived from their password. Both the client and the KDC derived the key from the password.


  • When the logon succeed the kerberos client calculate a cryptographic key from the password. This key is the long term key and is put in a secure volatile cache storage. 
  • The KDC also calculate a cryptographic key from the password, the algorithm calculation is the same thus the key is the same on both side. 
  • Notice that no password is exchanged between the workstation and the KDC on the network.

But we have two security problems ... 
  • The key Ka is a long term key. Every time A request an authentication the key Ka must be used. Then it's "easy" for a virus, a malware, any user who use the A session to get this long term key and then get a long term ability to connect to any service on the network that A was able to connect
  • When A request a kerberos ticket for D to the KDC, we must implement a pre-authentication mechanism. Otherwhise anybody could submit a request in the name of A. It should be ok as the response is encrypted with A's key thus only usable by A. But things are not so simple because the response also contains plaintext field that can be used to optimise a brute force attack on the key. Especially when the key is derived from a user password.
The solution is to create a special kerberos ticket called TGT (Ticket Granting Ticket) after the client successfully authenticate to the KDC.

The TGT is the ticket the client use for the KDC. It last about 8 hours (a logon session) and contains a session key for the communication between the client and the KDC, thus not using anymore the long term key. The long term key is used only during the authentication and is dropped from the memory.

This lead us to split the KDC into 2 different component :

  1. The authentication System (AS) that give a TGT to the client (TGT is a special kind of kerberos ticket for the KDC itself). 
  2. The Ticket Granting Service that give a kerberos ticket to the client for authenticating to the sevice
The AS hold the keys of the users (derived from the password) while the TGS hold the keys of the services : 



Now we can make the big picture :


If you want now to practise I advice to follow this tutorial on manula blog where you create a user, a KDC and a service. It's the opportunity to make the relation between the component/action and the big picture.

vendredi 8 mai 2015

Comprendre les problèmes d'encoding en java

Il m'arrive souvent d'intervenir sur les problèmes d'encoding. Le constat que je fais c'est que la plupart du temps les équipes connaissent mal les concepts autour de l'encoding, ce qui rend le debuggage laborieux.

Pourtant ça n'a rien de bien compliqué. Je vais d’abord vous faire un petit rappel des concepts puis ensuite je vous montrerai quelques exemples d'accidents, en général une fois qu'on a compris ce qu'il se passait la résolution n'est plus très loin.

2 concepts majeurs: l'encoding et la table des symbôles

La table des symbôles

La table des symbôles n'est pas un concept informatique à proprement parlé il s'agit d'un tableau ou pour être plus exact d'une liste exhaustive de tous les symbôles qu'une plateforme applicative décide de gérer il n'est pas question ici d'octet de mémoire ou autre mais juste de symbôles.

En java la table des symboles utilisée est l'unicode. Unicode est aujourd'hui un standard de l'industrie.


L'encoding

L'encoding c'est ce qui fait le lien entre le symbôle et l'octet. Par exemple le symbôle "é" e accent aigu à pour valeur  en octet 
  • En ISO-8859-1 :   -23
  • En UTF-8 :            -61 -87
  • En UTF-32 :          0 0 0 -23
Le é en unicode c'est à dire dans la table des symbole a pour valeur U00E9 en java voici le code que j'ai utilisé pour obtenir le résultat précédent : 

 public static void main(String[] args) {
  System.out.println(showBytes("\u00E9","ISO-8859-1"));
  System.out.println(showBytes("\u00E9","UTF-8"));
  System.out.println(showBytes("\u00E9","UTF-32"));
 }
 
 private static String showBytes(String str, String encoding){
  String result = "";
  try {
   for (byte b : str.getBytes(encoding)){
    result += " " + b;
   }
  } catch (UnsupportedEncodingException e) {
   e.printStackTrace();
  }
  return result;
 }

Les accidents

Supposons que j'écrive le "é" dans un fichier en utilisant l'encoding ASCII puis que je charge les byte du fichier dans un string en utiisant le mauvais encoding. Voici un exemple de ce que cela peut donner :



 On peut émuler toutes ces erreurs avec le code suivant
 
byte[] e_accent_aigu_en_ASCII = "\u00E9".getBytes("ASCII");
byte[] e_accent_aigu_en_ISO_8859_1 = "\u00E9".getBytes("ISO-8859-1");
byte[] e_accent_aigu_en_UTF_8 = "\u00E9".getBytes("UTF-8");
byte[] e_accent_aigu_en_UTF_32 = "\u00E9".getBytes("UTF-32");
  
System.out.println("ASCII byte, ASCII encoding = " + new String(e_accent_aigu_en_ASCII,"ASCII"));
System.out.println("ASCII byte, ISO-8859-1 encoding = " + new String(e_accent_aigu_en_ASCII,"ISO-8859-1"));
System.out.println("ASCII byte, UTF-8 encoding = " + new String(e_accent_aigu_en_ASCII,"UTF-8"));
System.out.println("ASCII byte, UTF-32 encoding = " + new String(e_accent_aigu_en_ASCII,"UTF-32"));
System.out.println("ISO-8859-1 byte, ASCII encoding = " + new String(e_accent_aigu_en_ISO_8859_1,"ASCII"));
System.out.println("ISO-8859-1 byte, ISO-8859-1 encoding = " + new String(e_accent_aigu_en_ISO_8859_1,"ISO-8859-1"));
System.out.println("ISO-8859-1 byte, UTF-8 encoding = " + new String(e_accent_aigu_en_ISO_8859_1,"UTF-8"));
System.out.println("ISO-8859-1 byte, UTF-32 encoding = " + new String(e_accent_aigu_en_ISO_8859_1,"UTF-32"));
System.out.println("UTF-8 byte, ASCII encoding = " + new String(e_accent_aigu_en_UTF_8,"ASCII"));
System.out.println("UTF-8 byte, ISO-8859-1 encoding = " + new String(e_accent_aigu_en_UTF_8,"ISO-8859-1"));
System.out.println("UTF-8 byte, UTF-8 encoding = " + new String(e_accent_aigu_en_UTF_8,"UTF-8"));
System.out.println("UTF-8 byte, UTF-32 encoding = " + new String(e_accent_aigu_en_UTF_8,"UTF-32"));
System.out.println("UTF-32 byte, ASCII encoding = " + new String(e_accent_aigu_en_UTF_32,"ASCII"));
System.out.println("UTF-32 byte, ISO-8859-1 encoding = " + new String(e_accent_aigu_en_UTF_32,"ISO-8859-1"));
System.out.println("UTF-32 byte, UTF-8 encoding = " + new String(e_accent_aigu_en_UTF_32,"UTF-8"));
System.out.println("UTF-32 byte, UTF-32 encoding = " + new String(e_accent_aigu_en_UTF_32,"UTF-32"));
  

Quelques points qu'il faut avoir en-tête

On voit que le "é" en ASCII n'existe pas, java l'a remplacé par le le byte du "?" en ASCII qui a pour valeur 63.

Quand java exécute String.equals,  String.contains ou String.match en interne il le fait symbole par symbole et non byte par byte comme peuvent le faire certains langage.

Enfin UTF-8 est un encodage qui peut être sur 1, 2, 3 ou 4 bytes ce qui signifie que les algorithmes qui vont interpréter les fichiers encodés en UTF-8 seront plus complexes à mettre en oeuvre que les algorithmes qui devront interpréter UTF-16 ou UTF-32 qui eux sont soit sur 2 ou 4 bytes pour chaque caractères. UTF-8 va partager le même encodage que ISO-8859-1 pour tous les caractères ascii ce qui permet en français de gérer des fichiers beaucoup plus légers car on ajoute des bytes en plus que pour les caractères non ascii.

Mais pour un texte entièrement en chinois ou en arabe UTF-32 permettra une interprétation plus rapide. Cela peut devenir très significatif pour un livre entier par exemple.

mardi 18 mars 2014

Comment gérer un dump mysql trop important

Je me suis retrouvé en production à devoir réinstaller un dump mysql très très lourd. Il faisait 22 Go.

Ce dump était très long a executer car il y avait des ordre d'insertion trop nombreux sur une table spécifique.

A chaque fois que je voulais ouvrir le fichier dans un éditeur de texte il plantait ou était inutilisable car la mémoire qui lui était alloué etait totalement pleine.

C'est ici qu'il faut sortir l'outil sed avec son option -i (inplace).

Bien entendu je recommande de faire un backup du fichier avant de se livrer à cette petite aventure.

 sed -i '/INSERT INTO `table_ou_il_y_a_trop_dinsertion`/c\-- removed' mondump.sql

La commande -i signifie inplace, le fichier sera changé directement sans passer par une copie intermédiare
le c\ signifie change, c'est donc toute la ligne ou cette expression a été trouvé qui sera changée.

mercredi 1 janvier 2014

Servlet Name doit être avant servlet class

En Créant un fichier web.xml j'ai eu une erreur étrange sur ce bout de code
<servlet>   
   <servlet-class>org.eclipse.jetty.proxy.ProxyServlet</servlet-class>
   <servlet-name>proxy</servlet-name>
</servlet>
Avec ce message

cvc-complex-type.2.4.a: Invalid content was found starting with element 'servlet-class'. One of 
 '{"http://java.sun.com/xml/ns/javaee":description, "http://java.sun.com/xml/ns/javaee":display-
 name, "http://java.sun.com/xml/ns/javaee":icon, "http://java.sun.com/xml/ns/javaee":servlet-
 name}' is expected.

En allant voir la xsd correspondantd à servlet, il est attendu que servlet name soit déclarer avant servlet class. Inverser les deux balises résout le problème.
<servlet>      
   <servlet-name>proxy</servlet-name>
   <servlet-class>org.eclipse.jetty.proxy.ProxyServlet</servlet-class>
</servlet>

samedi 7 décembre 2013

Client Server Bluetooth en java : le code commenté.

1. Préparation
2. Un peu de théorie sur bluetooth
3. Le code commenté
4. Le projet eclipse avec toutes les dépendances

Voici maintenant le code commenté du client et du server.

Le code client

package bluetooth;

import java.io.DataOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import javax.bluetooth.DeviceClass;
import javax.bluetooth.DiscoveryAgent;
import javax.bluetooth.DiscoveryListener;
import javax.bluetooth.LocalDevice;
import javax.bluetooth.RemoteDevice;
import javax.bluetooth.ServiceRecord;
import javax.bluetooth.UUID;
import javax.microedition.io.Connector;
import javax.microedition.io.StreamConnection;

/**
 * Le code client.
 * 
 * Ce code est à executer sur l'ordinateur qui sert de client.
 * 
 * Ce code met en oeuvre les trois étapes que nous avons décrites précédemment : 
 * 1 On découvre les appareils autour de nous (inquiry)
 * 2 Recherche de service sur les appareils trouvés.
 * 3 Utilisation du service 
 * 
 * On remarque que l'api thread est mis en oeuvre ici car la méthode startInquiry 
 * démarre dans un nouveau thread (thread enfant) mais le thread père doit disposer d'un
 * moyen de savoir quand le thread enfant à fini. Pour faire cela on utilise la technique dite du 
 * sémaphore. Si vous ne la connaissez pas je vous conseille de commencer par lire ces quelques explications :
 *  
 * http://www.javaworld.com/javaqa/1999-11/02-qa-semaphore.html.
 * 
 *  
 */
public class Client {

 /**
  * La liste des devices bluetooth trouvés.
  */
 public static final List devicesDiscovered = new ArrayList();

 /**
  * Le service que je recherche sur ce device, un service doit avoir un nom unique, on utilise 
  * un utilitaire uuidgen pour générer ces chaines de caractère uniques.
  * 
  * Sur le code du serveur c'est aussi cette chaine qui est utilisés pour publier le service.
  *  
  */
 final static String myServiceUUID = "2d26618601fb47c28d9f10b8ec891363";
 
 /**
  * L'uid doit respecter certaine règle comme par exemple être positif la chaine doit faire 
  * moins de 32 caractère etc. La classe UUID contrôle ces règles lors de la construction.
  */
 final static UUID MYSERVICEUUID_UUID = new UUID(myServiceUUID, false);
 
 /**
  * La liste des services que l'on souhaite obtenir sur le devices. 
  */
 final static UUID[] searchUuidSet = new UUID[] { MYSERVICEUUID_UUID };
 
 /**
  * Lorsque l'on obtient le service, à la demande du client le serveur peut fournir 
  * des sortes de métadonnées (pair clé-valeur) appelés attributs. On pourrait imaginer un client 
  * qui voudrait envoyer de la musique à une chaine stéréro souhaiterai connaitre la liste des
  * formats musicaux supportés par le services. Sans spécificiation une liste d'attributs par défaut est fourni.
  * @see javax.bluetooth.DiscoveryAgent.searchServices(int[] attrSet, UUID[] uuidSet, RemoteDevice btDev, DiscoveryListener discListener)
  */
 final static int[] attrIDs = new int[] { 0x0100 }; // Service handle
 
 public static void main(String[] args) throws IOException,
   InterruptedException {

  
  //inquiryCompletedEvent est un semaphore 
  //qui bloquera le thread a l'appel de sa méthode wait 
  //et qui le poursuivra a l'appel de sa méthode notifyAll
  final Object inquiryCompletedEvent = new Object();
  
  //serviceSearchCompletedEvent est un semaphore 
  //qui bloquera le thread a l'appel de sa méthode wait 
  //et qui le poursuivra a l'appel de sa méthode notifyAll
  final Object serviceSearchCompletedEvent = new Object();

  // on vide la listes des périphériques bluetooth que l'on va trouver
  devicesDiscovered.clear();

  //le listener qui déclenchera les actions quand des devices et les services seront trouvés.
  DiscoveryListener listener = new MyDiscoveryListener(devicesDiscovered, inquiryCompletedEvent, serviceSearchCompletedEvent);
  
  
  //dabord on cherche les devices 
  synchronized (inquiryCompletedEvent) {
   // on passe le listener à la méthode start inquiry, cette appel 
   // revient tout de suite car il a lieu dans un autre thread.
   boolean started = LocalDevice.getLocalDevice().getDiscoveryAgent()
     //GIAC correspond au profil générique.
     .startInquiry(DiscoveryAgent.GIAC, listener);
   if (started) {
    System.out.println("wait for device inquiry to complete...");
    // wait met en attente le thread principal qui attend 
    // maintenant qu'un autre thread le notifie.
    inquiryCompletedEvent.wait();
    System.out.println(devicesDiscovered.size()
      + " device(s) found");    
   }
  }
  
    //une fois les devices trouvés on cherche les services sur ces devices
    for (RemoteDevice rm : devicesDiscovered){
   synchronized (serviceSearchCompletedEvent) {
    System.out.println("search services on " +  rm.getBluetoothAddress() + " " + rm.getFriendlyName(false));
             LocalDevice.getLocalDevice().getDiscoveryAgent().searchServices(attrIDs, searchUuidSet, rm, listener);
    serviceSearchCompletedEvent.wait();
    System.out.println("Fin de la recherche de services");
   }
  }
  
  
  
  
 }

}

/**
 * on cree un listener que l'on passera à la méthode startInquiry pour
 * mieux comprendre le pattern listener
 * http://rom.developpez.com/java-listeners/.
 * Ce listener sera notifié a
 * chaque fois qu'un device sera découvert. Notifié signifie en fait que
 * ses méthodes seront appelées à chaque fois que le système découvrira
 * un device. On va donc redéfinir les méthodes pour mettre en oeuvre
 * nos actions à chaque dévouverte.
 * 
 * Les méthodes deviceDiscovered et inquiryCompleted du listener s'applique lors de l'inquiry,
 * alors que les méthodes servicesDiscovered et serviceSearchCompleted s'applique 
 * lors de la recherche de service sur un device. Je pense qu'ils auraient du créer 
 * deux listeners car ces deux actions sont quand même différentes. 
 * 
 *
 */
class MyDiscoveryListener implements DiscoveryListener {
 
 private List devicesDiscovered;
 
 private Object inquiryCompletedEvent;
 
 private Object serviceSearchCompletedEvent;
 
 
 /**
  * 
  * @param devicesDiscovered la liste des devices trouvés.
  * @param inquiryCompletedEvent le semaphore pour la recherhe de device.
  * @param serviceSearchCompletedEvent le semaphore pour la recherche de service.
  */
 public MyDiscoveryListener(List devicesDiscovered, Object inquiryCompletedEvent, Object serviceSearchCompletedEvent){
  this.devicesDiscovered = devicesDiscovered;
  this.inquiryCompletedEvent = inquiryCompletedEvent;
  this.serviceSearchCompletedEvent = serviceSearchCompletedEvent;
 }

 /**
  * Méthode qui sera appelee à chaque fois qu'un device est decouvert.
  */
 public void deviceDiscovered(RemoteDevice btDevice, DeviceClass cod) {
  System.out.println("Device " + btDevice.getBluetoothAddress()
    + " found");
  // on alimente la liste des devices trouvés pour pouvoir
  // exploiter
  // cette information ultérieurement.
  devicesDiscovered.add(btDevice);
  try {
   System.out.println("     name "
     + btDevice.getFriendlyName(false));
   System.out.println("Blutooth Adress "  + btDevice.getBluetoothAddress());
  } catch (IOException cantGetDeviceName) {
   cantGetDeviceName.printStackTrace();
  }
 }

 /**
  * Méthode appelée quand le système estime avoir fini son travail
  * d'inquiry pour nous cela signifie que notre liste de device est
  * complète.
  */
 public void inquiryCompleted(int discType) {
  if (INQUIRY_COMPLETED==discType){
   System.out.println("Device Inquiry completed!");
   synchronized (inquiryCompletedEvent) {
    //on notifie cette objet pour permettre 
    //au thread principal de revenir de sa méthode wait.
    inquiryCompletedEvent.notifyAll();
   }
  }else if (INQUIRY_ERROR==discType){
   System.out.println("inquiry request failed to complete normally, but was not cancelled.");
  }else if (INQUIRY_TERMINATED==discType){
   System.out.println("device discovery has been canceled by the application and did not complete.");
  }
 }

 

 /**
  * Quand l'ensemble des services publiés que vous avez souhaité
  * retrouver sont découverts cette méthode est appelée.
  * 
  * A chaque service correspond un service record c'est pourquoi 
  * on reçoit un tableau, qui correspond au tableau de service que vous avez demandé 
  * lors de l'appel de  javax.bluetooth.DiscoveryAgent.searchServices(int[] attrSet, UUID[] uuidSet, RemoteDevice btDev, DiscoveryListener discListener)
  * 
  * Si sur le device serveur vous n'avez publié 
  * qu'un seul service vous  recevrez logiquement un tableau avec un seul élément.
  * 
  * Au sein de ces services record se trouve des serviceAttribute. 
  * Les attributs par défaut sont fourni mais il est possible de 
  * demander à recevoir d'autres attributs. 
  * 
  * @see javax.bluetooth.DiscoveryAgent.searchServices(int[] attrSet, UUID[] uuidSet, RemoteDevice btDev, DiscoveryListener discListener)
  * 
  */
 public void servicesDiscovered(int transID,
   ServiceRecord[] servRecords) {
      
  for (ServiceRecord sr : servRecords){
   System.out.println("Service trouvé " + sr.getConnectionURL(ServiceRecord.NOAUTHENTICATE_NOENCRYPT, false) );
   
   //je vais maintenant pouvoir parler au sevice !!!!
   System.out.println("Envoie du message en cours");
   //on récupère l'url du service 
   //on pourrait sauvegarder cette url 
   //pour un acces direct au service sans passer
   //par la phase d'Inquiry
         String connectionURL =  sr.getConnectionURL (
                 ServiceRecord.NOAUTHENTICATE_NOENCRYPT, false);
         try {
             System.out.println(
                 "Connection a " + sr.getHostDevice().getFriendlyName(false) + 
                 ", " + connectionURL);
             StreamConnection streamConnection =  (StreamConnection) Connector.open (connectionURL);
             DataOutputStream dataout = 
                 streamConnection.openDataOutputStream();
             //maintenant j'envoie hello world au serveur.
             dataout.writeUTF("Hello World");
             System.out.println("Message envoye, fermeture du canal");
             streamConnection.close();
         } catch (IOException ioe) {
             System.out.println(
                 " exception & + ioe");
         }
  }
 }
 
 /*
  * Cette méthode sera appelée quand la recherche de service 
  * sera finie. Comme dans cette exemple on parle immédiatement 
  * au service dès qu'on le trouve. elle ne nous sera pas d'une 
  * grande utilité.
  *  
  * @see javax.bluetooth.DiscoveryListener#serviceSearchCompleted(int, int)
  */
 public void serviceSearchCompleted(int transID, int respCode) {
  System.out.println("Service search completed!");
  synchronized (serviceSearchCompletedEvent) {
   //on notifie cette objet pour permettre 
   //au thread principal de revenir de sa méthode wait.
   serviceSearchCompletedEvent.notifyAll();
  }
 }
}


Le code Serveur

package bluetooth;

import java.io.DataInputStream;
import java.io.IOException;

import javax.bluetooth.DiscoveryAgent;
import javax.bluetooth.LocalDevice;
import javax.bluetooth.RemoteDevice;
import javax.bluetooth.UUID;
import javax.microedition.io.Connector;
import javax.microedition.io.StreamConnection;
import javax.microedition.io.StreamConnectionNotifier;

/**
 * C'est un serveur bluetooth il ne fait que répéter dans la console 
 * le message qu'il a reçu.
 * 
 * Ce code est a executer sur l'ordinateur qui sert de serveur.
 * 
 * @author michael
 *
 */
public class Server {
 
 public static void main(String[] args) throws IOException {
  // le nom du service car l'uuid n'est pas très parlant ...
  final String myServiceName = "echoService";
  /**
   * Le service que je publie sur ce device, un service doit avoir un nom unique, on utilise 
   * un utilitaire uuidgen pour générer ces chaines de caractère uniques.
   * 
   * Sur le code du client c'est aussi cette chaine qui est utilisés pour obtenir le service.
   *  
   */
  final String myServiceUUID = "2d26618601fb47c28d9f10b8ec891363";
  UUID MYSERVICEUUID_UUID = new UUID(myServiceUUID, false);

  // Définit l'url du service.
  //localhost car on est le serveur 
  String connURL = "btspp://localhost:"+MYSERVICEUUID_UUID.toString()+";name="+myServiceName;
 
  //on se rend découvrable 
  LocalDevice.getLocalDevice().setDiscoverable(DiscoveryAgent.GIAC);
  
  System.out.println("Creation d'un service " + connURL);
  
  // On publie le service record dans le SRDB (Service record database)
  StreamConnectionNotifier scn = (StreamConnectionNotifier)  
                                           Connector.open(connURL);                   
 
  // On accepte la connexion d'un client, tant qu'aucun client 
  //ne frappe à la porte cette méthode bloque.
  StreamConnection sc = scn.acceptAndOpen();
  //on lit ce que le client nous envoie puis on l'écrit dans la console.
  DataInputStream dataIn = sc.openDataInputStream();
  String s = dataIn.readUTF();
  System.out.println("Echo > " + s);
  sc.close();
  scn.close();
 }
 
 

}

dimanche 17 novembre 2013

Client Server Bluetooth en java : un peu de théorie.

1. Préparation
2. Un peu de théorie sur bluetooth
3. Le code commenté
4. Le projet eclipse avec toutes les dépendances

Cette partie a pour but de voir le minimum necessaire sur la spécification bluetooth pour pouvoir comprendre les apis logiciels que nous mettons en oeuvre dans ce tutoriel. Ce résumé à ma sauce s'appuie sur :



Le dernier article est très complet et je vous conseille de l'approfondir pour étudier l'utilisation du JSR 82.

Le stack bleutooth 


Ce sont les couches logicielles qui nous interesse et ne parlerons pas des couches matérielles. 

L2CAP
L2CAP (Logical Link Control & Adaptation Protocol) fournit les services de multiplexage des protocoles de niveau supérieur et la segmentation et le réassemblage des paquets ainsi que le transport des informations de qualité de service. Les protocoles de haut niveau peuvent ainsi transmettre et recevoir des paquets jusqu'à 64 Ko. Elle autorise un contrôle de flux par canal de communication.

La couche L2CAP utilise des canaux logiques.
Dans le java bluetooth stack cette couche est représenté par l'interface L2CAPConnection. Comme elle respecte le CLDC Generic Connection Framework (http://www.oracle.com/technetwork/systems/index-155711.html) elle est accompagnée de L2CAPConnectionNotifier qui permet de créer des connections de type server. 

En pratique on ne travaillera pas directement avec L2CAPConnection mais plustôt avec l'interface StreamConnection et StreamConnectionServer qui respecte les mêmes contrats. En effet travailler L2CAPConnection nous oblige à limiter la taille de nos paquets et à gérer le réassemblage de paquets. Néanmoins voici un exemple d'utilisation de des interfaces.   
//coté client 
//.....
 L2CAPConnection conn = (L2CAPConnection) 
//on suppose que le server écoute sur le port 1003 on verra qu'en fait les services 
//sont découverts dynamiquement.
          Connector.open("btl2cap://0050CD00321B:1003;ReceiveMTU=512;TransmitMTU=512");
PrintStream out = new PrintStream(conn.openOutputStream());
//...

//coté serveur 
//.....
L2CAPConnectionNotifier service = (L2CAPConnectionNotifier) 
           Connector.open("btl2cap://localhost:0050CD00321B;ReceiveMTU=512;TransmitMTU=512");
L2CAPConnection conn = (L2CAPConnection) service.acceptAndOpen();
InputStream is = conn.openInputStream();


Comme nous le disions précédemment "Elle autorise un contrôle de flux par canal de communication" c'est pourquoi on trouve dans ces deux urls les parametres ReceiveMTU et TransmitMTU :

/**
 * ReceiveMTU
 * specifies the maximum payload size this connection can accept, and
 * TransmitMTU specifies the maximum payload size this connection can
 * send
*/



RFCOMM 

Il est exigé d'un stack bluetooth qu'au dessus de sa couche L2CAP un protocole série soit émulé : RFCOMM (Radio frequency communication).

Cette exigence connu aussi sous Serial Port Profile (SPP) existe car beaucoup de processus savent communiquer avec ce protocole plutôt qu'avec L2CAP.

Par exemple pour les tests sur un PC on peut utiliser Hyper Terminal pour voir les données arrivant sur le port émulé COM.

Je ne présente pas d'exemple d'utilisation avec RFCOMM car je pense que c'est la même chose que pour L2CAP. On trouve deux classes : BluetoothRFCommConnection et BluetoothRFCommConnectionNotifier qui implémentent respectivement StreamConnection et StreamConnectionNotifier que nous allons étudier plus en détail.

 Je n'ai néanmoins pas approfondi cette partie et toute remarque est bienvenue.

SDP 

Service Discovery protocole, qui permet de découvrir les autres devices à portée du client. Le code du précédent article présente ce processus et est déjà abondemment commenté.

TCS 

Telephony Control Protocol, est un protocole obsolète orienté donnée binaire, je n'ai pas cherché à l'étudier, si vous pouvez apporter plus de précision sur son rôle c'est avec plaisir que je lirai vos commentaires.

OBEX

 Obex est un protocole permettant l'échange d'objet, nous n'allons pas l'étudier dans ce tutoriel, mais vous trouverez deux exemples de codes : 

Client Obex : http://bluecove.org/apidocs/overview-summary.html#OBEXPutClient
Server Obex : http://bluecove.org/apidocs/overview-summary.html#OBEXPutServer

Les profils

Un profil correspond à une spécification fonctionnelle d'un usage particulier. Les profils peuvent également correspondre à différents types de périphériques.

Les profils ont pour but d'assurer une interopérabilité entre tous les appareils Bluetooth. Ils définissent : la manière d'implémenter un usage défini les protocoles spécifiques à utiliser les contraintes et les intervalles de valeurs de ces protocoles.

On a vu un de ces profils SPP (Serial Port Profile), lorsqu'un insdustriel souhaite obtenir la certification Bluetooth il doit valider une certaine quantités de ces profils.

La notion de client et de serveur.

Il faut bien différencier la notion de client et de serveur, un serveur publie un service et un client le consomme. Un device peut à la fois être client et serveur, aucun stack ne met de limite à être soit uniquement client soit uniquement serveur.

Voici un diagramme d'activité qui illustre ce que nos deux codes (code client et code serveur) vont faire.



Le code client.

Il va commencer par découvrir les devices bluetooth autour de lui : Un casque bluetooth, un téléphone, un autre ordinateur, etc.

C'est la phase d'inquiry, il va envoyer des ondes tout autour de lui pour dire qu'il cherche à découvrir les autres devices. Si les autres devices ont décidé de se rendre découvrable alors ils répondront à ce message d'inquiry pour dire qu'ils sont présents. Cela permet au client d'acquerir les adresses physiques des devices autour de lui et lui permet de les mettre dans son cache.

Enfin une fois qu'il aura découvert ces devices il va demander s'il existe un service spécifique sur le devices auquel il s'adresse. Si la réponse est positive alors la communication va avoir lieu.

Le code serveur

Il publie un service en ajoutant donc une entrée (Un service record) dans le SDB (Service Data Base) puis il se met en attente d'une requète entrante.

Et La sécurité et l'appairage des appareils ?


Cette partie n'est pas de la responsabilité des codes serveur ou client. C'est le statck sous-jacent qui doit s'occuper de l'appairage et de l'authentification des appareils entre eux.

Au mieux les codes peuvent exiger que l'échange n'ai lieu que si les appareils se sont authentifiés et/ou que l'échange soit cryptés, mais ce n'est pas eux qui  géreront cette partie.