Inovia Blog

L’actualité des technologies innovantes

La détection de présence avec un Raspberry Pi 3 et Node-RED

Par Kadda SAHNINE

Publié le | 17 avril 2016 | Pas de commentaire

Node-RED


Comme de nombreux fans, je suis un heureux possesseur d’un Raspberry Pi 3, le dernier né de la fondation Raspberry Pi, intégrant en standard les technologies sans fil WiFi 802.11 b/g/n et Bluetooth Low Energy 4.1.
Ce concentré de technologies constitue une plateforme idéale pour une centrale domotique bon marché et un objet d’étude fort intéressant pour quiconque s’intéresse au monde de l’Internet des Objets (IoT).

Depuis longtemps déjà, j’avais le projet de réécrire le code de ma solution domotique “maison” avec Node-RED, un fantastique outil permettant d’élaborer des scénarios d’intégration complexes combinant des objets physiques (capteurs, micro-contrôleurs, transceivers, etc.) et des services en ligne.

Dans cet article, j’exposerai une solution de détection de présence (et d’absence) via Bluetooth fonctionnant aussi bien avec un smartphone ou un tracker Bluetooth bon marché glissé dans un cartable par exemple.
Les cas d’utilisation sont très nombreux. Citons par exemple :

  • la réception d’un itinéraire conseillé en transports en commun en sortant de chez soi
  • l’envoi d’une notification lorsque ses enfants sont bien rentrés à la maison
  • le verrouillage/déverrouillage d’une alarme
  • le pilotage d’une prise radio-commandée

Node-RED a vu le jour sous les hospices d’IBM Emerging Technologies Group à des fins de prototypage rapide de solutions IoT. Le code source a été publié en licence libre fin 2013.
Il offre un modèle de développement normalisé, orienté message, et une interface graphique permettant de décrire un flux de traitements.

Il existe une extension Node-RED fournissant un support Bluetooth (basée sur le module nodeJS noble) mais je n’ai pas trouvé le résultat satisfaisant, l’extension étant beaucoup trop expérimentale.

J’ai opté pour le développement en Python d’un service de découverte de périphériques Bluetooth, publiant vers Node-RED les évènements d’absence ou de présence dans un socket TCP (Node-RED propose en standard un module d’écoute TCP). Cette solution fonctionne très bien.
Le code source est disponible sur mon dépôt Github.

Mise en oeuvre

Node-RED est disponible par défaut sur les dernières versions de Raspbian Jessie ou installable via le gestionnaire de paquets APT :

sudo apt-get update
sudo apt-get install nodered

Procéder ensuite aux étapes suivantes :

  • installer les dépendances :
    sudo apt-get install bluetooth python-yaml python-bluetooth
    
  • recopier la configuration et les flux Node-RED mis en oeuvre dans le cadre de l’article :
    wget -P ~/.node-red https://github.com/ksahnine/rpi-home-automation/blob/master/.node-red/settings.js
    wget -P ~/.node-red https://github.com/ksahnine/rpi-home-automation/blob/master/.node-red/flows.json
    
    Attention : les 2 commandes ci-dessus remplacent votre configuration et vos flux Node-RED ! Faites l’opération manuellement si vous partez d’une configuration existante.
  • installer l’application de détection de périphériques :
    git clone https://github.com/ksahnine/rpi-home-automation.git
    
  • configurer le fichier rpi-home-automation/ble2tcp/devices.yml
  • démarrer Node-RED puis le programme de détection de périphériques Bluetooth :
    node-red
    python rpi-home-automation/ble2tcp/ble2node-red.py -c rpi-home-automation/ble2tcp/devices.yml
    

L’interface Node-RED est accessible par défaut sur le port 1880 du Raspberry Pi (ex: http://rumah.local:1880/)

Comment ça marche

Les périphériques à surveiller sont définis dans un fichiers de configuration (devices.yml) au format YAML :

Un scan est effectué périodiquement toutes les 4 secondes.

Note : l’adresse Bluetooth d’un périphérique peut être récupérée via la commande hcitool scan sous Debian après avoir installé le paquet bluetooth (sudo apt-get install bluetooth).

Lorsque l’état d’un périphérique change (présence ou absence), un évènement est publié au format JSON dans un socket TCP :

L’évènement est ensuite lu en entrée du flux de traitement Node-RED :

Node-RED flow


Il est constitué de l’enchaînement de noeuds suivants :

  • noeud BLE to Node TCP scanner : implémente un listener TCP écoutant les évènements de présence ou d’absence des périphériques surveillés.
    Node-RED TCP

  • noeud json : transforme l’évènement en objet JSON
  • noeud Présence ? : switch orientant le traitement en fonction de la présence ou de l’absence d’un périphérique
    Node-RED switch

  • noeud Arrivé : traite l’évènement émis lorsqu’un périphérique est proche du foyer
    Node-RED debug node

  • noeud Sorti : traite l’évènement émis lorsqu’un périphérique quitte le foyer

React ou l’art de la composition

Par Kadda SAHNINE

Publié le | 28 janvier 2016 | Pas de commentaire

Logo React


Eric Clemmons a publié sur Medium un billet d’humeur remarqué, intitulé JavaScript Fatigue, une lourde charge contre React et l’écosystème JavaScript.
Eric y déplore à juste titre la pénibilité d’initialiser un projet React, l’usage frénétique de boilerplates ou de générateurs de codes, la pléthore d’outils et d’API JavaScript en compétition les uns avec les autres, etc.

Tout cela est vrai. L’honnête développeur ou l’évangéliste JavaScript ne peuvent qu’acquiescer mais le réquisitoire contre React me paraît très injuste.
Pour appuyer cette conviction, je vous propose de découvrir les qualités de React à travers le développement d’un widget affichant les prochains passages du métro parisien dans une station donnée :

Widget RATP

Le rendu du widget est issu du code JSX ci-dessous :

On notera que le composant expose les propriétés suivantes :

  • type : le type de réseau (métro, rer, etc.)
  • line : le numéro de ligne
  • station : l’identifiant de la station
  • direction : l’identifiant de la direction

Le code source du widget est disponible sur mon dépôt GitHub.
Les données du trafic proviennent de l’excellente API REST de Pierre Grimaud

Mise en oeuvre pour les impatients

Si vous souhaitez tester localement ce projet, installez préalablement npm / node puis suivez le mode opératoire suivant :

git clone https://github.com/ksahnine/ratp-widget-react.git
cd ratp-widget-react
npm install
npm run dev
open http://localhost:8080
Note : je vous conseille d’utiliser npm/node à travers le gestionnaire de version nvm.

La structure du projet est la suivante :

├── app
│   ├── components
│   │   ├── RatpHeader.jsx
│   │   ├── RatpStatusBar.jsx
│   │   ├── RatpWidget.jsx
│   │   └── css
│   │       └── ratp-widget.css
│   └── main.js
├── build
│   └── index.html
├── package.json
└── webpack.config.js

React : en bref

React est une librairie JavaScript orientée composant développée par Facebook, permettant de construire des interfaces utilisateur modulaires et très performantes.
Elle jouit depuis plus d’un an d’une popularité grandissante, à telle enseigne que certains développeurs Angular envisagent sérieusement une conversion.
React encourage le développement d’interfaces utilisateur par composition, c’est-à-dire par assemblage de composants graphiques réutilisables pouvant eux mêmes être issus d’une composition. Ce modèle de conception est en train d’émerger dans le monde du développement des interfaces web et constitue une avancée majeure dans l’effort d’une plus grande standardisation des développements.

React présente au moins 3 avantages comparatifs :

  • la performance grâce au concept de DOM virtuel
  • l’approche composants
  • la possibilité de construire des applications universelles

Le DOM virtuel

Manipuler un arbre DOM directement via son API est compliqué. Suivre ses changements d’état est encore plus difficile et nécessite de développer des stratégies qui sont loin d’être neutres vis-à-vis des performances.
Angular 1 par exemple utilise la technique du dirty checking. Tous les objets du modèles sont surveillés en permanence, y compris ceux ne changeant jamais d’état.

Angular DOM

C’est une des raisons pour lesquelles Angular 1 pose des problèmes de performances, bien perceptibles sur des supports mobiles comme les tablettes.

React quant à lui implémente le concept de DOM virtuel, une structure de données JavaScript représentant l’arbre DOM dans un état donné. Lorsque le modèle de données évolue, React génère un nouveau DOM virtuel dans son intégralité, reflétant le nouvel état du modèle.

React DOM virtuel

React calcule ensuite le différentiel entre les 2 DOM virtuels et applique les modifications minimales à l’arbre DOM. Cette phase très performante est appelée réconciliation.

Orienté composant

Le principe de séparation des concepts (separation of concerns) appliqué au développement web consiste traditionnellement à séparer le code HTML, CSS et JavaScript. C’est un principe de division technologique.

Mais ce n’est pas la seule façon d’interpréter ce principe. L’approche de React est radicalement différente car elle consiste à développer des composants unitaires où template et logique de présentation sont encapsulés dans une même unité de développement en JavaScript.
Examinons cet exemple de composant React implémentant une fiche :

Ce qui peut décontenancer de prime abord, c’est l’impression de mélanger JavaScript, HTML et même CSS. Or ce n’est qu’une vue de l’esprit car tout est écrit en JavaScript, le code JSX n’étant que du sucre syntaxique pour rendre le code plus lisible.
Dans cette approche, le principe de séparation des responsabilités est plutôt fonctionnel, chaque composant remplissant une fonction élémentaire et exposant une interface bien définie (propriétés).

Applications universelles (ou isomorphiques)

Angular a popularisé le développement d’applications SPA (Single Page Application). Néanmoins, en plus de poser des problèmes de performance (l’ancien client web de Twitter en est un exemple fameux), cette architecture présente également l’inconvénient de rendre les sites non indexables par les moteurs de recherche.
React fonctionne aussi bien sur un navigateur (SPA) que sur un serveur (node.JS) ouvrant ainsi la voie au développement d’applications dites universelles (ou isomorphiques), c’est-à-dire pouvant s’exécuter de manière concomitante du côté client ou du côté serveur.

Pourquoi je crois en React

Au delà des qualités propres à React, je crois en la viabilité de cette librairie du seul fait de la pérennité du language JavaScript, quasiment la seule compétence requise pour développer avec React, alors que le développement d’une vue avec Angular ou tout autre framework MVC nécessite l’assimilation de nombreuses spécificités syntaxiques, par ailleurs changeantes au fil des versions (ex. ng-repeat Angular 1 devient ngFor sous Angular 2).
En outre, alors qu’Angular est plutôt centré sur HTML, React est totalement centré sur JavaScript (ES2015/ES6), incomparablement plus puissant que HTML.

Même si React est un projet encore jeune et que son écosystème fait penser à de la lave en fusion, je ne saurais trop vous conseiller de prendre ce projet au sérieux, ne serait-ce que pour pratiquer ECMAScript 6 et adopter un angle de développement tout à fait nouveau dans le monde des interfaces web.

Initialisation d’un projet : le trio React - Babel - webpack

C’est incontestablement la tâche la plus pénible. Elle a de quoi irriter beaucoup de développeurs tant elle peut être complexe, obscure et chronophage.
Nous utiliserons le gestionnaire de paquets npm pour installer les nombreux modules constituant la chaîne de développement et dont voici le descripteur package.json :

Essayons de démystifier tout cela. Les principaux composants utilisés dans la chaîne de développement sont :

React

Le projet utilise React 0.14.6, la version la plus récente publiée à ce jour.
Depuis la version 0.14, React a été découpé en deux paquets npm :

  • react : le coeur de la librairie React
  • react-dom : ce module est utilisé pour générer/synchroniser l’arbre DOM lors du rendu des composants React. L’opération peut se faire côté client (ReactDOM.render()) ou côté serveur (ReactDOMServer.renderToString()), ouvrant ainsi la voie au développement d’applications isomorphiques

L’idée de ce redécoupage est de mutualiser le code de React, ayant vocation à être multiplateforme (web ou mobile via React Native).

Babel

Même s’il est techniquement possible de développer un projet React en ECMAScript 5 (la version de JavaScript supportée par tous les navigateurs actuels), on tirera un énorme profit d’ECMAScript 6 tant les innovations de cette version simplifieront le développement.
On utilisera Babel 6.0, un transcompilateur JavaScript (ou compilateur source à source) transformant le code source ES6 en ES5.
Dans sa 6ème version, le code de Babel a été profondément réorganisé en plugins indépendants afin d’en faire une plateforme modulaire et ouverte vers d’autres outils ou langages.
Dans notre cas, nous utiliserons les modules :

  • babel-core : l’API node.JS de Babel
  • babel-loader : ce module est utilisé par webpack pour automatiser la transcompilation du code via Babel
  • babel-preset-es2015 : ensemble de plugins Babel (d’où le préfixe preset) pour le support ECMAScript 6.
  • babel-preset-react : ensemble de plugins Babel pour le support de React (la syntaxe JSX essentiellement).
  • babel-runtime : helpers et polyfills Babel

L’activation des presets babel-preset-es2015 et babel-preset-react nécessite de définir le fichier de configuration .babelrc à la racine du projet et avec le contenu suivant :

{
  "presets": ["es2015", "react"]
}

Webpack

Webpack est à la fois un gestionnaire de tâche et un assembleur de modules. Grâce à webpack, le code du projet peut être organisé en modules autonomes sans se soucier des problématiques de dépendances ou de packaging. On utilisera les packages npm webpack et webpack-dev-server.
Dans notre cas d’utilisation, webpack coordonne :

  • la transformation du code JSX en JavaScript
  • la transcompilation du code source ES6 en ES5
  • l’assemblage du projet et de ses dépendances en une seule unité de déploiement (dans notre cas, le fichier JavaScript bundle.js)

Webpack

Le module webpack-dev-server implémente un serveur HTTP local avec rechargement à chaud en cas de modification du code source.

Enfin, on utilisera également les 2 plugins webpack suivants :

  • css-loader :ce plugin permet “d’importer” une feuille de style CSS depuis un composant React
  • style-loader : ce plugin insère dynamiquement le code CSS d’une feuille de style importée dans une balise HTML style
Note : On aurait pu se passer de ces 2 plugins dans le cadre de notre projet.

Les règles d’assemblage du projet sont définies dans le fichier de configuration webpack.config.js situé à la racine du projet :

  • entry définit le point d’entrée de l’application (fichier app/main.js).
  • output définit le fichier résultant de l’assemblage des composants et de leurs dépendances (fichier build/bundle.js).
  • resolve.extensions contient un tableau des extensions identifiant les modules à assembler.
  • module.loaders contient 2 chargeurs, l’un traitant des imports CSS et l’autre du code JSX ainsi que de la transformation ES6/ES5 des fichiers contenus dans le répertoire app

L’application SPA est chargée dans le navigateur via le fichier statique build/index.html depuis un serveur web (ou plus exactement webpack-dev-server en mode développement) :

Développement du widget RatpWidget

Décomposition

Le schéma ci-dessous décrit un choix de décomposition du widget (RatpWidget) avec la hiérarchie de composants suivante :

  • RatpHeader : un cartouche affichant le logo de la ligne de métro, la station et la destination
  • RatpStatusBar : un message décrivant l’état du trafic sur la ligne

Décomposition du widget

RatpStatusBar

Le composant RatpStatusBar est très simple. Il affiche l’état du réseau défini par la propriété message du composant :

RatpStatusBar

Il est implémenté sous la forme d’un module ES6 afin de pouvoir être importé :

  • la directive import charge la librairie React.
  • le mot clé ES6 export permet d’exporter le composant au reste de l’application sous la forme d’un module.
  • l’API React.createClass() permet de définir un nouveau composant React.
  • la méthode render() implémente le rendu du composant, décrit par la syntaxe JSX. React invoque cette méthode dès que l’état ou une propriété du composant est modifié.
  • on notera à la ligne 11 que la propriété message du composant est utilisé pour afficher l’état du trafic ({this.props.message}).

RatpHeader

Passons au composant RatpHeader affichant le cartouche dont voici un exemple d’utilisation :

RatpHeader

Sa structure n’est pas très différente de celle du composant RatpStatusBar :

On remarquera à la ligne 7 que l’URL des icônes est dynamique et s’appuie sur de très pratiques template Strings, une fonctionnalité d’ES6.

RatpWidget

Analyson le code du composant (RatpWidget.jsx) :

  • 3 méthodes de l’API React sont utilisées :
    • getInitialState() retourne l’état initial du composant. Dans la terminologie React, un état (this.state) est un objet JavaScript accessible en lecture écriture et modélisant un ensemble de propriétés internes au composant.
    • render() implémente le rendu du composant. On notera l’utilisation des sous composants RatpHeader et RatpStatusBar
    • componentDidMount() est une méthode invoquée à l’initialisation du widget, juste avant l’appel à la méthode render(). Dans le cas d’espèce, on récupère les données du trafic via un appel AJAX.
  • Ligne 18 : la méthode fetchData récupère les données du trafic et met à jour l’état du widget (setState()).
    Note : l’architecture Flux (et en particulier le framework Redux) est un modèle de développement beaucoup plus adaptée à une application web.
  • Ligne 58 : les données sont mise à jour sur le clic du bouton de rafraichissement (onClick={this.fetchData}). Ainsi, l’état du composant est mis à jour et React invoque la méthode render() retournant le nouveau rendu.

Point d’entrée de l’application

Passons à la phase finale. Le point d’entrée de l’application est implémenté dans le fichier app/main.js :

On notera en particulier :

  • ligne 3 : import du composant React RatpWidget
  • ligne 8 : l’API RenderDOM.render() génère le rendu du composant RatpWidget (1er paramètre) dans l’arbre DOM (à l’intérieur du conteneur div passé dans le 2nd paramètre).

L’assemblage et la publication du projet est effectué par webpack via la commande npm run dev :

Sécuriser une API REST avec JSON Web Token (JWT)

Par Kadda SAHNINE

Publié le | 26 novembre 2015 | Pas de commentaire

JWT


Mon article consacré la spécification CORS a eu un certain succès d’audience. Il me semble opportun de le compléter avec un nouvel article traitant de la sécurisation d’une API REST.
J’ai choisi d’aborder JSON Web Token (JWT, que les anglophones prononcent jot), un standard ouvert permettant à deux parties d’échanger de manière sûre des informations encapsulées dans un jeton signé numériquement.
En pratique, JWT est utilisé pour mettre en oeuvre des solutions d’authentification SSO ou de sécurisation de web services.

Bien que le protocole OAuth 2 soit très utilisé par des plateformes à forte audience exposant une API web, JWT apparait dans beaucoup de cas d’utilisation comme une alternative intéressante car beaucoup plus simple à mettre en oeuvre et stateless (ce qui rend la solution scalable).

Après avoir décrit la structure et l’utilisation d’un “jot“, nous verrons en pratique comment sécuriser une API REST construite sur Node.js et le service de gestion d’identité Stormpath. Le code source accompagnant cet article est disponible sur mon dépôt GitHub.

Structure d’un jeton JWT

Un jeton JWT est une chaîne de caractères décomposable en 3 sections séparées par un point (.).

Jeton JWT

  • En-tête : c’est un document au format JSON, encodé en base 64 et contenant des méta-données. Il doit contenir au minimum le type de jeton et l’algorithme de chiffrement utilisé pour le signer numériquement.
    Exemple :

    {
      "typ": "JWT",
      "alg": "HS256"
    }
    
  • Charge utile : cette section est un document au format JSON encodé en base 64, contenant des données fonctionnelles minimales que l’on souhaite transmettre au service (ces propriétés sont appelées claims ou revendications selon la terminologie de la RFC). En pratique, on y fait transiter des informations sur l’identité de l’utilisateur (login, nom complet, rôles, etc.). Il ne doit pas contenir de données sensibles. Voici un exemple :
    {
      "iat": 1448383708,
      "exp": 1448383768,
      "name": "compte de test",
      "username": "demo",
      "email": "demo@foobar.com"
    }
    

    A noter que l’on distingue 3 types de claims :

    • claims réservés : il s’agit de noms réservés par la spécification et ne pouvant être utilisés par le développeur. Par exemple, iat contient la date de génération du jeton et exp sa date d’expiration.
    • claims publics : il s’agit de noms normalisés dont on encourage l’utilisation (ex. name, email, phone_number). Le registre est maintenu par l’organisation IANA et est consultable sur leur site.
    • claims privés : il s’agit de noms à usage privé pour répondre à des besoins spécifiques à vos applications. Ils ne doivent pas entrer en conflit avec les autres types de claims.
  • Signature : cette zone contient la signature numérique du jeton. La clé privée utilisée pour signer le jeton est stockée côté serveur.

Fonctionnement et étude de cas

De par son format compact et sa nature stateless (le jeton n’est pas stocké dans une base de données), JWT est très adapté aux transactions HTTP.
Ainsi, dans la requête d’accès à une ressource protégée, le jeton est véhiculé dans l’en-tête Authorization avec le mécanisme d’authentification Bearer :

Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImRlbW8iLCJmdWxsTmFtZSI6

Le schéma ci-dessous représente un dialogue entre un client (navigateur ou autre) et une API REST (et CORS compatible) exposant 2 services :

  • un service d’authentification : POST /api/authenticate
  • un service à accès restreint retournant une liste de comptes : GET /api/restricted/accounts

Cinématique HTTP

  • dans un premier temps, le client cherche à accéder à une ressource protégée sans utiliser de jeton. Le service retourne une erreur avec un code HTTP 401 (Unauthorized)
  • dans un deuxième temps, le client s’authentifie. Le service vérifie que les vecteurs d’accréditation sont corrects, génère un jeton JWT avec une durée de vie prédéfinie puis retourne la réponse sous la forme d’un document JSON contenant le jeton (attribut token).
  • muni de ce jeton, le client accède à nouveau à la ressource protégée en le propageant dans l’entête Authorization. Le service vérifie que le jeton est effectivement valide puis retourne la liste des comptes.
    A noter que si cette requête avait été émise au delà de la durée d’expiration du jeton, le service retournerait une erreur HTTP 401 (jeton invalide).

Le code source de cette API ainsi que les instructions d’installation sont disponibles sur mon dépôt GitHub, les comptes utilisateur étant stockés dans Stormpath.

Installation et utilisation de l’API

Le mode opératoire d’installation est le suivant (testé sous Node.js 0.12) :

git clone https://github.com/ksahnine/jwt-secured-api-node.git
cd jwt-secured-api-node
npm install

Pour démarrer le conteneur de services, exécuter la commande :

npm start

Voici quelques cas d’utilisation de l’API via l’utilitaire cURL :

  • Cas 1 : accès à une ressource protégée sans jeton :
    $ curl http://localhost:8000/api/restricted/accounts
    {
      "status": "error",
      "msg": "Jeton invalide",
      "url": "/api/restricted/accounts"
    }
    
  • Cas 2 : authentification avec récupération d’un jeton :
    $ curl -X POST --data "username=demo1&password=Demo2015" http://localhost:8000/api/authenticate
    {
      "status": "ok",
      "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImRlbW8xIiwiZn"
    }
    
  • Cas 3 : accès à une ressource protégée avec un jeton valide :
    $ curl -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImRlbW8xIiwiZn" \
            http://localhost:8000/api/restricted/accounts
    [
      {
        "id": 0,
        "name": "Account 0"
      },
      {
        "id": 1,
        "name": "Account 1"
      }
    ]
    

Description de la pile technique

L’API est construite au dessus de Node.js et s’appuie les modules suivants :

  • le framework web Express pour l’implémentation des services REST/JSON et la gestion du routage
  • le module node cors pour le support CORS (compatible avec Express)
  • le middleware Passport pour la gestion de l’authentification sous Node.js, ainsi que le plugin passport-stormpath pour l’accès au gestionnaire d’identité Stormpath
  • le module jsonwebtoken pour générer et signer numériquement les jetons JWT
  • le module Express express-jwt pour valider les jetons JWT lors de l’accès aux ressources protégées
A l’attention du développeur AngularJS : vous n’êtes pas sans savoir que la spécification CORS interdit d’utiliser conjointement l’en-tête Access-Control-Allow-Origin: * et la propriété XHR withCredentials: true. Dans cette configuration, il n’est par exemple pas possible de propager un cookie de session. Dans le cas d’espèce, la solution adaptée serait de générer une entête Access-Control-Allow-Origin dynamique mais JWT présente une alternative bien plus intéressante. Nous y reviendrons dans un prochain article.
keep looking »

A propos de l'auteur

Kadda SAHNINE
Architecte technique Java EE
#JavaEE #Linux #vim addict #OpenSoftware #OpenHardware
Voir le profil de Kadda Sahnine sur LinkedIn

Flux RSS

Rechercher

Administration