Authentification et WebSocket, avec Node.JS, Express, et Socket.IO

ATTENTION : certaines portions de code présentées ici sont valables pour Express 2.x et ne fonctionnent plus avec Express 3.x. Il s’agit principalement de la partie traitant du parsing de cookie, l’articles reste à 95% valable, mais vous trouverez les détails pour Express 3 dans cet article → Authentification WebSocket avec les sessions Express 3.x et Socket.IO

La première fois qu’on utilise les websockets, on se dit que c’est compliqué (pas tant que ça en fait). Puis on utilise socket.io, et on se dit que c’est vraiment super simple :) Et puis un jour on aimerait bien sécuriser la connexion au socket, parce que la partie « temps réel » nécessite d’être authentifié, ou simplement parce qu’il faut un pré-requis (comme avoir donné son nom avant de pouvoir chatter) avant que l’écoute ne démarre réellement.

La première idée qu’on a est de se trimballer des flags et d’activer/désactiver l’écoute de certains messages en fonction de ces flags (par exemple: tant que l’utilisateur n’a pas donné son nom, les évènements « message envoyé » sont ignorés). Le problème est que la connexion est pourtant déjà ouverte, des données y transitent déjà, et c’est donc vraiment très loin d’être idéal. De manière plus générale, il semble très complexe de communiquer avec la session utilisateur au sein du websocket. En effet on a un code de ce genre, où l’on voit bien qu’il semble impossible de manipuler la session utilisateur dans le cadre d’un websocket vu qu’il n’y a aucune notion de « request »:

sockets.on('connection', function (socket) {
  // But...
  socket.on('msg', function (data) {
    // ... where the fuck is my session?
  });
});

On va voir que le protocole est bien fait et que la connexion se fait au sein d’une enveloppe HTTP standard, et qu’on a donc accès à tous les headers d’une requête HTTP, notamment les cookies, permettant de récupérer par exemple un ID de session, et donc de lire/modifier la session utilisateur depuis le websocket!

Sommaire:

  1. Gestion des sessions avec Express
  2. Sécuriser une page
  3. Utilisation de Socket.IO
  4. Manipulation de la session utilisateur depuis un WebSocket, et sécurisation de la connexion
  5. Gestion des connexions mutiples
  6. Aller plus loin…

Note: si vous connaissez déjà bien Express et Socket.IO, vous êtes encouragé à sauter directement au chapitre 4 (éventuellement, le chapitre 2 peut rester intéressant). Le reste ne vous apprendra rien.

Retrouvez le code complet des exemples sur GitHub.

Pré-requis

Je pars du principe dans cet article que vous connaissez:

  • Express, le micro-framework web pour Node.JS;
  • Le principe des middlewares dans Express;
  • Les Web Sockets, et (au moins de nom) la librairie « socket.io »;
  • Ah et puis le protocole HTTP, la notion de requête, les cookies, tout ça tout ça…

Si ce n’est pas le cas: GO L2P N00B!

On commence par installer les modules pré-requis: npm install express ejs socket.io.

Puis on crée l’arborescence minimal d’un projet Express (on ajoutera des fichiers au fur et à mesure):

/
+- app.js
+- views/
   +- layout.ejs

1. Gestion des sessions avec Express

Voici notre application de départ app.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const path = require('path')
    , express = require('express')
    , app = module.exports = express.createServer()
    , port = process.env.PORT || 1337
    ;
 
/** Configuration */
app.configure(function() {
  this.set('views', path.join(__dirname, 'views'));
  this.set('view engine', 'ejs');
  this.use(express.static(path.join(__dirname, '/public')));
});
app.configure('development', function(){
  this.use(express.errorHandler({ dumpExceptions: true, showStack: true }));
});
app.configure('production', function(){
  this.use(express.errorHandler());
});
 
/** Routes */
app.get('/', function (req, res, next) {
  res.render('index');
});
 
/** Start server */
if (!module.parent) {
  app.listen(port)
}

Les sessions avec Express fonctionnent de manière habituelle: un ID de session est affecté à un visiteur et stocké dans un cookie. Côté serveur, on stocke des données dans un « store » variable (mémoire, fichiers, base de données…) affectées à cet ID, avec un petit garbage collector qui va bien. Rien de détonnant, néanmoins comme par défaut il n’y a aucun parsing des headers de la requête HTTP, il faut tout de même prévoir quelques opérations ne serait-ce que pour savoir lire le cookie ;)

On va donc utiliser deux « middlewares »: un pour parser les cookies, un autre pour gérer les sessions. C’est dans la partie « configuration » que ça se passe:

6
7
8
9
10
11
12
13
14
15
16
17
18
19
/** Configuration */
app.configure(function() {
  ...
  // Allow parsing cookies from request headers
  this.use(express.cookieParser());
  // Session management
  this.use(express.session({
    // Private crypting key
    "secret": "some private string",
    // Internal session data storage engine, this is the default engine embedded with connect.
    // Much more can be found as external modules (Redis, Mongo, Mysql, file...). look at "npm search connect session store"
    "store":  new express.session.MemoryStore({ reapInterval: 60000 * 10 })
  }));
});

Une fois qu’on a fait ça, chaque requête est enrichie d’un objet « session » qu’on peut manipuler. Supposons qu’on veuille par exemple attribuer un index numérique à un utilisateur, et l’incrémenter à chaque fois qu’il arrive sur la page (grand classique des sessions n’est-ce pas):

Template: views/session-index.ejs

1
2
3
4
<ul>
  <li>ID de session: <%= sessId %>
  <li>Index: <%= index %>
</ul>

Route: /session-index

31
32
33
34
35
36
37
38
39
app.get('/session-index', function (req, res, next) {
  // Increment "index" in session
  req.session.index = (req.session.index || 0) + 1;
  // View "session-index.ejs"
  res.render('session-index', {
    "index":  req.session.index,
    "sessId": req.sessionID
  });
});

On lance l’application (node app.js) et on se rend sur http://127.0.0.1/session-index. On doit avoir un résultat de ce genre:

À chaque rafraichissement, l’index augmente. Ouvrez la page dans un autre navigateur et vous aurez un ID de session différent et un retour à la valeur 1.

Voilà pour la mise en place du support des sessions. Maintenant on va les utiliser de manière un peu plus pertinente, par exemple pour gérer une connexion et des pages interdites aux utilisateurs non connectés.

2. Sécuriser une page

On va se doter de deux pages:

  • « / » est un chat, qui requiert d’être identifié;
  • « /login » présente un simple formulaire avec un champ texte pour le login (pas de mot de passe ici);

Template views/index.ejs:

1
<p>Hello, <%= username %></p>

Route /:

41
42
43
44
45
46
47
48
49
app.get('/', function (req, res, next) {
  if (req.session.username) {
    // User is authenticated, let him in
    res.render('index');
  } else {
    // Otherwise we redirect him to login form
    res.redirect("/login");
  }
});

Comme il est fort probable qu’on aura dans notre application finale plusieurs pages à « sécuriser », on va extraire cette partie dans un middleware qu’on pourra ainsi réutiliser partout où c’est nécessaire:

41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/** Middleware for limited access */
function requireLogin (req, res, next) {
  if (req.session.username) {
    // User is authenticated, let him in
    next();
  } else {
    // Otherwise, we redirect him to login form
    res.redirect("/login");
  }
}
 
/** Home page (requires authentication) */
app.get('/', [requireLogin], function (req, res, next) {
  res.render('index');
});

Le code semble plus lisible ainsi: le fait qu’une  route soit sécurisé n’est plus traité au sein de son callback, mais comme un filtre préliminaire. C’est même conceptuellement bien plus proche de la réalité d’une sécurisation de page.

Maintenant passons au formulaire d’identification.

Template views/login.ejs:

1
2
3
4
5
6
<p>You need to be logged in, bastard!</p>
<% if (error) { %><p>Erreur: <%= error %></p><% } %>
<form method="post">
  <input type="text" name="username" placeholder="Type your login" autofocus required<% if (username) { %> value="<%= username %>"<% } %>>
  <input type="submit">
</form>

Afin d’être capable de lire le contenu d’un formulaire, il faut parser le corps de la requête, et un middleware est là pour ça:

22
23
  // Allow parsing form data
  this.use(express.bodyParser());

Route /login:

59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
/** Login form */
app.get("/login", function (req, res) {
  // Show form, default value = current username
  res.render("login", { "username": req.session.username, "error": null });
});
app.post("/login", function (req, res) {
  var options = { "username": req.body.username, "error": null };
  if (!req.body.username) {
    options.error = "User name is required";
    res.render("login", options);
  } else if (req.body.username == req.session.username) {
    // User has not changed username, accept it as-is
    res.redirect("/");
  } else if (!req.body.username.match(/^[a-zA-Z0-9\-_]{3,}$/)) {
    options.error = "User name must have at least 3 alphanumeric characters";
    res.render("login", options);
  } else {
    // Validate if username is free
    if (usernameIsAlreadyUsed) {
      options.error = "User name is already used by someone else";
      res.render("login", options);
    } else {
      req.session.username = req.body.username;
      res.redirect("/");
    }
  }
});

Le formulaire vérifie d’abord que le nom d’utilisateur entré a le bon format, puis s’il n’est pas déjà utilisé, et si tout va bien affecte la donnée en session et redirige vers la page d’accueil. Dans le cas contraire on affiche une erreur. Reste à implémenter la partie « le nom est déjà utilisé »: Dans un cas « réel » on aurait une base de données et une identification par login/password. Ici on va se contenter de parcourir le SessionStore pour voir si personne avec une session active ne serait déjà en train d’utiliser ce nom:

76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
// Validate if username is free
req.sessionStore.all(function (err, sessions) {
  if (!err) {
    var found = false;
    for (var i=0; i<sessions.length; i++) {
      var session = JSON.parse(sessions[i]); // Si les sessions sont stockées en JSON
      if (session.username == req.body.username) {
        err = "User name already used by someone else";
        found = true;
        break;
      }
    }
  }
  if (err) {
    options.error = ""+err;
    res.render("login", options);
  } else {
    req.session.username = req.body.username;
    res.redirect("/");
  }
});

Note: la méthode all() est spécifique au MemoryStore de connect, et ne se retrouvera pas forcément sur les autres stores.

On y est :) Redémarrez l’application, et rendez-vous sur http://127.0.0.1:1337, vous serez redirigé vers /login et là vous pouvez choisir un nom. Si vous ouvrez un autre navigateur sur la même adresse, et tentez d’utiliser le même nom vous devriez vous faire envoyer dans les roses.

Maintenant qu’on a sécurisé notre page côté serveur, on va y mettre quelque chose d’intéressant, et de tellement original: un chat \o/

3. Utilisation de Socket.IO

Bon, si vous êtes habitués des tutos, et que vous vous êtes déjà tapé le chat en WebSocket avec Node.JS au moins mille fois, vous pouvez sauter cette étape, elle ne vous apprendra rien.

Côté serveur, dans app.js:

99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
/** WebSocket */
var sockets = require('socket.io').listen(app).of('/chat');
sockets.on('connection', function (socket) { // New client
  var socket_username = null;
  // User sends his username
  socket.on('user', function (username) {
    socket_username = username;
    sockets.emit('join', username, Date.now());
  });
  // When user leaves
  socket.on('disconnect', function () {
    if (socket_username) {
      sockets.emit('bye', socket_username, Date.now());
    }
  });
  // New message from client = "write" event
  socket.on('write', function (message) {
    if (socket_username) {
      sockets.emit('message', socket_username, message, Date.now());
    } else {
      socket.emit('error', 'Username is not set yet');
    }
  });
});

Côté client, dans views/index.ejs:

3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<pre id="messages" style="max-height:300px; overflow:auto;"></pre>
<form method="post" id="message-form">
  <input type="text" placeholder="Type your message" id="message-input" name="message" autofocus required>
  <input type="submit" value="OK">
</form>
 
<script type="text/javascript" src="/socket.io/socket.io.js"></script>
<script type="text/javascript">
/** Socket */
var sio = io.connect(), socket = sio.socket.of('/chat');
socket
.on('connect', function () {
  // at connection, first send my username
  socket.emit('user', <%- JSON.stringify(username) %>);
})
.on('join', function (username, time) {
  // someone joined room
  addMessage(username, 'joined room', time);
})
.on('bye', function (username, time) {
  // someone left room
  addMessage(username, 'left room', time);
})
.on('error', function (error) {
  // an error occured
  alert('Error: ' + error);
})
.on('message', function (username, message, time) {
  // someone wrote a message
  addMessage(username, message, time);
});
 
/** Form */
var messageInput = document.getElementById('message-input');
document.getElementById('message-form').onsubmit = function () {
  socket.emit('write', messageInput.value);
  messageInput.value = '';
  return false;
}
 
/** Display */
var messagesArea = document.getElementById('messages');
function addMessage(username, message, time) {
  if (typeof time != 'string') {
    var date = new Date(time);
    time = date.getHours() + ':' + date.getMinutes() + ':' + date.getSeconds();
  }
  var line = '[' + time + '] <strong>' + username + '</strong>: ' + message + '<br />';
  messagesArea.innerHTML = line + messagesArea.innerHTML;
}
</script>

Voici comment ça se passe pour l’instant:

  1. L’utilisateur arrive sur « / », il est renvoyé sur « /login » pour spécifier son nom d’utilisateur;
  2. Il est alors redirigé sur « / », et là un websocket est ouvert;
  3. Il reçoit l’évènement « connect », et émet en retour l’évènement « user » en spécifiant son nom d’utilisateur;
  4. Le serveur reçoit l’évènement « user », stocke le nom d’utilisateur pour plus tard, et broadcaste l’évènement « join » pour prévenir tout le monde que cet utilisateur est arrivé;
  5. Quand l’utilisateur essaie d’envoyer un message, ça émet l’évènement « write » avec son message;
  6. Le serveur reçoit l’évènement « write », et s’il connaît le nom d’utilisateur du client alors c’est bon: on broadcaste l’évènement « message »;
  7. En revanche en cas de faille spatio-temporelle où l’utilisateur aurait réussi à envoyer un message avant que l’évènement « user » ne soit correctement traité, il recevra une erreur;
  8. Quand l’utilisateur coupe la connexion, le serveur broadcaste un « bye » pour prévenir tout le monde de son départ;

On détecte immédiatement plusieurs problèmes:

  • À l’ouverture du socket, l’utilisateur envoie son username, ce qui n’est pas optimal: on a déjà une session, on aimerait simplement l’utiliser; de plus, le fait de devoir commencer par envoyer son username alors que le socket est déjà ouvert impose de faire des vérifications partout;
  • Si l’utilisateur ouvre un nouvel onglet sur la même page, il a toujours la même session, mais ouvre un nouveau socket, soit un nouveau message « join » pour tout le monde, et un « bye » quand il fermera l’onglet alors même qu’il est toujours connecté sur un autre socket;

Le premier problème relève de l’utilisation de la session depuis un websocket, soit le but de cet article.

Le second problème en revanche est classique et il n’y a hélas pas de méthode pour réutiliser une connexion déjà ouverte. Il faut donc stocker quelque part le fait qu’un même utilisateur a X connexions WebSocket ouvertes, et savoir les identifier. L’idéal serait de les stocker dans la session, mais pour ça il faut déjà savoir comment accéder à la session depuis le WebSocket ;) Here we go!

4. Manipulation de la session utilisateur dans une connexion WebSocket

Lors de l’ouverture du WebSocket, la première étape est une franche poignée de main entre le client et le serveur. Le client devant alors décliner son identité, il présente une enveloppe HTTP standard avec des headers contenant notamment les cookies. C’est en lisant ces cookies qu’on va pouvoir récupérer l’ID de session, et en interrogeant le moteur de stockage qu’on va pouvoir récupérer la session liée à un ID (via la méthode get(), cette fois-ci commune à tous les moteurs de stockage de session compatibles connect).

Côté serveur, il faut donc dans un premier temps rendre accessible le moteur de stockage de sessions à tous les acteurs de notre application. La partie où l’on déclare le middleware de gestion de session va donc être modifié ainsi:

14
15
16
17
18
19
20
21
22
  // Session management
  // Internal session data storage engine, this is the default engine embedded with connect.
  // Much more can be found as external modules (Redis, Mongo, Mysql, file...). look at "npm search connect session store"
  this.sessionStore = new express.session.MemoryStore({ reapInterval: 60000 * 10 });
  this.use(express.session({
    // Private crypting key
    "secret": "some private string",
    "store":  this.sessionStore                                            
  }));

On peut donc accéder à app.sessionStore pour lire et manipuler les sessions partout dans notre application, et notamment dans le WebSocket. On va donc maintenant implémenter la couche « poignée de main » :)

On va avoir besoin d’un parseur de cookies, et on va donc utiliser celui de connect. Mais comme Express ne présente pas toute l’API de connect de manière publique, on va d’abord devoir installer le module: npm install connect.

Notre création de socket va s’enrichir de ces quelques lignes auto-documentées:

102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
const parseCookie = require('connect').utils.parseCookie;
sockets.authorization(function (handshakeData, callback) {
  // Read cookies from handshake headers
  var cookies = parseCookie(handshakeData.headers.cookie);
  // We're now able to retrieve session ID
  var sessionID = cookies['connect.sid'];
  // No session? Refuse connection
  if (!sessionID) {
    callback('No session', false);
  } else {
    // Store session ID in handshake data, we'll use it later to associate
    // session with open sockets
    handshakeData.sessionID = sessionID;
    // On récupère la session utilisateur, et on en extrait son username
    app.sessionStore.get(sessionID, function (err, session) {
      if (!err && session && session.username) {
        // On stocke ce username dans les données de l'authentification, pour réutilisation directe plus tard
        handshakeData.username = session.username;
        // OK, on accepte la connexion
        callback(null, true);
      } else {
        // Session incomplète, ou non trouvée
        callback(err || 'User not authenticated', false);
      }
    });
  }
});

ATTENTION : dans Express 3.x, le cookie de session est signé. Voir ici pour les détails → Authentification WebSocket avec les sessions Express 3.x et Socket.IO

Le fonctionnement détaillé:

  1. Tous les détails sont sur cette page du wiki de Socket.IO;
  2. On utilise la méthode authorize() comme indiqué dans le wiki, car on est dans le cas d’un socket utilisant un namespace;
  3. Cette méthode prend en paramètre une fonction de traitement de l’authentification, qui prend elle-même deux paramètres:
    1. Les données de la « poignée de main », concrètement l’enveloppe de la requête HTTP;
    2. Une fonction de callback à appeler une fois le traitement terminé avec deux paramètres: une éventuelle erreur et un booléen indiquant si on accepte ou non la connexion;
  4. Lorsqu’on utilise ce processus d’autorisation, la connexion elle-même est « suspendue » tant qu’on n’a pas finalisé cette étape, et accepté ou non l’ouverture du socket;

Ainsi on a une vraie étape de validation de la connexion qui prend en compte la session utilisateur, et qui interdit réellement la connexion en cas d’échec. On a d’un côté énormément gagné en sécurité (le socket n’est pas ouvert si la session n’est pas complète) mais aussi en simplicité par la suite (plus besoin de cet échange et de ces vérifications autour du username: on l’a déjà!). Le reste du code de notre socket s’en voit en effet réduit.

Côté serveur:

129
130
131
132
133
134
135
136
137
138
139
sockets.on('connection', function (socket) { // New client
  sockets.emit('join', socket.handshake.username, Date.now());
  // When user leaves
  socket.on('disconnect', function () {
    sockets.emit('bye', socket.handshake.username, Date.now());
  });
  // New message from client = "write" event
  socket.on('write', function (message) {
    sockets.emit('message', socket.handshake.username, message, Date.now());
  });
});

Côté client, on peut supprimer la ligne socket.emit('user', ...) qui est devenue inutile.

Bilan: sécurisation de la connexion, et suppression des échanges redondants!

5. Gestion des connexions multiples

Comme on l’a vu, il va falloir trouver un moyen d’associer la session utilisateur et les connexions ouvertes par l’utilisateur. On aurait pu faire cette association en se basant sur le username, mais on préfèrera une information réellement robuste comme l’ID de session. Ainsi on ne se prendra pas les pieds dans le tapis si on veut plus tard autoriser les utilisateurs à modifier leur nom.

Quand l’utilisateur ouvre un socket, on l’ajout à sa liste de connexions déjà ouvertes, et cette fois on ne broadcaste l’évènement « join » que si c’est sa première connexion:

129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
// Active sockets by session
var connections = {};
sockets.on('connection', function (socket) { // New client
  var sessionID = socket.handshake.sessionID; // Store session ID from handshake
  // this is required if we want to access this data when user leaves, as handshake is
  // not available in "disconnect" event.
  var username = socket.handshake.username; // Same here, to allow event "bye" with username
  if ('undefined' == typeof connections[sessionID]) {
    connections[sessionID] = { "length": 0 };
    // First connection
    sockets.emit('join', username, Date.now());
  }
  // Add connection to pool
  connections[sessionID][socket.id] = socket;
  connections[sessionID].length ++;
...

Quand il ferme ce socket, on oublie la connexion, et cette fois-ci on ne broadcaste l’évènement « bye » que s’il vient de fermer sa dernière connexion:

143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
  // When user leaves
  socket.on('disconnect', function () {
    // Is this socket associated to user session ?
    var userConnections = connections[sessionID];
    if (userConnections.length && userConnections[socket.id]) {
      // Forget this socket
      userConnections.length --;
      delete userConnections[socket.id];
    }
    if (userConnections.length == 0) {
      // No more active sockets for this user: say bye
      sockets.emit('bye', username, Date.now());
    }
  });
...

Maintenant un même utilisateur (désigné par sa session) va pouvoir ouvrir le chat dans autant d’onglets qu’il le souhaite, il sera considéré comme déconnecté seulement lorsque le dernier onglet sera fermé.

6. Aller plus loin…

Dans les étapes suivantes, on pourrait:

  • Sécuriser complètement la connexion en passant par une couche SSL; c’est très simple à mettre en place car NodeJS et Express supportent l’https en natif, et socket.io (depuis sa version 0.6.10) dispose d’exemples de websockets over SSL;
  • Créer son propre moteur de stockage de session pour le maîtriser complètement et ne pas forcément tout avoir en mémoire à chaque instant;
  • Et même encore mieux, profiter de la toute nouvelle API de Socket.IO socket.get() et socket.set() qu’on peut a priori brancher sur n’importe quel moteur… dont notre moteur de stockage de session personnaligé évidemment ;) ce qui ferait une API de communication websocket <-> session d’une élégance imbattable;

Conclusion

Retrouvez le code complet des exemples sur GitHub.

On a vu qu’il était très simple de gérer une authentification en amont de la connexion, et donc de sécuriser un WebSocket de manière efficace. Avec la couche d’abstraction offerte par Socket.IO, et le récent ajout des namespaces (pour avoir plusieurs flux de communication distincts sur un seul socket) on a une solution vraiment élégante pour le temps réel qui ne finit pas de s’améliorer. Vous n’avez plus aucune excuse pour ne pas utiliser cette technologie :)

Handshake your booty!

32 réflexions au sujet de « Authentification et WebSocket, avec Node.JS, Express, et Socket.IO »

  1. wizad

    Article intéressant. Petite remarque cependant, en cherchant des informations sur ce même sujet il semble que le transport flashsocket ne transmette pas les cookies définis et donc pas de possibilité de récupérer la session (je n’ai pas encore testé mais j’ai vu plusieurs fois cette remarque). Si confirmé il faudrait alors envisager de désactiver le support de ce mode dan l’application souhaitant utiliser les sessions.

    Répondre
    1. naholyr Auteur de l’article

      Je n’ai effectivement pas testé tous les transports je m’étais contenté de websocket, long polling et streaming. Je vais vérifier ça et mettre à jour l’article.
      Merci pour cette info!

      Répondre
    2. naholyr Auteur de l’article

      J’ai donc fait le test, et non c’est bon :) pour les navigateurs qui supportent flashsocket (qui est désactivé par défaut), ça passe bien par la méthode « handshake », et j’ai bien hanshakeData.headers qui est disponible et contient la même chose que pour les autres transports :)

      Le test effectué:

      var sockets = require('socket.io').listen(app).configure(function () {
        this.set('transports', ['flashsocket']); // OK sous Firefox, Chrome ne connecte pas
      //  this.set('transports', ['websocket']);
      //  this.set('transports', ['htmlfile']); // Pas testé, je n'ai pas IE sous la main
      //  this.set('transports', ['xhr-polling']);
      //  this.set('transports', ['jsonp-polling']);
      }).of('/chat').authorization(function (handshakeData, callback) {
        console.log('authorization');
        console.log(handshakeData.headers);
        callback(null, true);
      }).on('connection', function() {
        console.log('connection');
      });

      C’est donc OK, même pour « flashsocket » :)

      Répondre
  2. Popy

    Pour la transmission du cookie, rien n’empèche de mettre coté client un petit
    socket
    .on('connect', function () {
    // at connection, first send my username
    socket.emit('handshake', <%- JSON.stringify(username) %>);
    })
    dans tous les cas.

    Par contre, je trouve que t’as fait un truc bizarre :

    connections[sessionID] = { "length": 0 };

    Pk pas connections[sessionID] = []; ?? .push() ??

    Répondre
    1. naholyr Auteur de l’article

      Si parce que c’est justement ce qu’on veut éviter, le fait de devoir faire un premier envoi de données avec le sockets ouvert, parce que ça veut dire que partout ailleurs dans tous les échanges il faut vérifier si ce premier envoi a bien eu lieu. Chose qu’on cherche justement à supprimer ;)

      Sinon pour le { "length": 0 } c’est parce que je veux un tableau associatif, afin de retrouver les clés rapidement. Mais je veux aussi avoir rapidement la notion de longueur. Je préfère me trimballer un length sachant qu’il n’y a qu’un endroit où je supprime/ajoute des données, plutôt que de d’utiliser indexOf() à la place de [key].

      Répondre
      1. Will

        Salut Naholyr,
        Et merci pour cet excellent tutoriel, un gros travail, chapeau… je dévore les autres, c’est que du bonheur : )

        Une remarque par rapport à ce premier envoi de message: ok, le serveur a bien repéré son client dés le handshake, pas besoin au client de lui redire notamment une fois la connection bien établie…

        Mais le client, à sa connection, comment sait-il qu’il est lui même? Par exemble, si je veux afficher mes lignes en bleu dans la fenetre de chat (on oublie la partie formattage conditionnel), comment le client peut-il reconnaitre ses propres lignes? Faut-il que le serveur lui retourne son ID dés la connexion ? Ou les informations socket.handshake.sessionID accédées dans sockets.on(‘connection’, function (socket){…}) sont-elles disponibles également côté client? avec les mêmes propriétés?

        Répondre
        1. naholyr Auteur de l’article

          Pas besoin d’avoir un ID spécifique, un simple flag peut faire l’affaire pour distinguer les évènement destiné au client lui-même ou des évènements plus génériques:

          Par exemple:

          
          // -- Serveur --
          
          // Broadcast (tout le monde sauf moi)
          socket.broadcast.emit('event', {"data": data});
          // Évènement lancé au socket lui-même
          socket.emit('event', {"data": data, "myself": true});
          
          // -- Client --
          
          socket.on('event', function (message) {
            if (message.myself) {
              alert('From server by myself: ' + message.data);
            } else {
              alert('From server by someone: ' + message.data);
            }
          });
          

          Selon le besoin de toute façon tu auras forcément un moyen d’identification: le nickname dans le cas d’un chat par exemple. Pas besoin d’avoir spécifiquement un ID dédié au socket, une information qui a plus de sens fonctionnel fera aussi bien voire mieux l’affaire :)

          Répondre
  3. Ping : Tutoriel Socket.IO (débutant) | Atinux

  4. Me

    Bonjour, merci pour ce post très instructif.

    Ca marche très bien avec firefox et chrome, mais j’ai un problème avec Opera: quand le browser essaie de connecter la socket, le serveur plante avec le message d’erreur suivant:

    node_modules/connect/lib/utils.js:132

    , pairs = str.split(/[;,] */);

    ^

    TypeError: Cannot call method ‘split’ of undefined

    (opera 11.51, ubuntux64)

    Répondre
    1. Bastien Tanese

      Plop !

      J’ai la même erreur, j’ai pensé que ça devait venir des dernières versions de Connect et Socket.IO.

      En regardant de plus près, j’ai trouvé que c’était Chrome qui n’envoyait pas de Cookie lors d’une connexion WebSocket.
      En affichant dans la console

      JSON.stringify(handshakeData)

      dans le authorization j’ai obtenu ceci (print_r() en PHP pour que ça soit plus clair):

      stdClass Object
      (
      [headers] => stdClass Object
      (
      [host] => 94.23.114.102:8000
      [connection] => keep-alive
      [user-agent] => Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.835.186 Safari/535.1
      [accept] => */*
      [referer] => http://94.23.114.102/v1/
      [accept-encoding] => gzip,deflate,sdch
      [accept-language] => fr-FR,fr;q=0.8,en-US;q=0.6,en;q=0.4
      [accept-charset] => ISO-8859-1,utf-8;q=0.7,*;q=0.3
      )

      [address] => stdClass Object
      (
      [address] => 86.76.99.173
      [port] => 50896
      )

      [time] => Tue Sep 27 2011 13:33:59 GMT+0400 (MSD)
      [query] => stdClass Object
      (
      [t] => 1317116039388
      [jsonp] => 0
      )

      [url] => /socket.io/1/?t=1317116039388&jsonp=0
      [xdomain] =>
      [issued] => 1317116039415
      )

      Du coup, je suis à court d’idée par rapport à l’authentification par cookie etc là… Help :) ?

      Répondre
  5. Yannik

    Bonjour,

    je suis votre tuto mais j’ai un soucis dés le début.

    Quand j’essaie d’afficher session-index, je n’ai rien qui s’affiche et je n’ai pas d’erreur dans le terminal o_O

    Si vous avez une idée, je suis preneur :)

    Répondre
  6. Erwyn

    Très bon tuto qui m’a permis de comprendre beaucoup de choses sur express js et son fonctionnement qui jusq’alors m’échappait.

    Keep the good work

    Répondre
  7. CélesteDC

    Bonjour,

    est ce qu’il est possible que le côté serveur soit fait avec C++ sous Qt ?

    Merci et très bonnes explications

    Répondre
  8. Rose

    Heidi-This is such a great resource . I can’t tell you how many times I’d fogret the steps to take to do this! Now I know exactly where I can look when I’m trying to remember next time Love it, thank you!!xoChristineChristine Ross recently posted..

    Répondre
  9. k33g_org

    ça fait un moment que j’avais bookmarké ton article, et voilà je viens enfin de m’en servir, eh bien un GRAND MERCI, tout particulièrement en ce qui concerne la sécurisation directe des routes.

    Répondre
  10. Stan

    Hello, super tutoriel, cela dit je m’en suis servi pour une authentification avec node 0.8.x, express 3.x et un connect assez récent et plus rien ne marche, j’ai du faire pas mal de changements (initialisation de l’appli, ajouter cookie, modifier la méthode parseCookie qui n’existe plus etc), tout semble OK mais le handshake ne se fait plus. Comptes-tu faire une version à jour? Si tu veux je peux t’envoyer la version « récente » qui comporte cette erreur… pour info cela met une popup error: client not handshaken

    Encore merci dans tous les cas cela m’a bien servi!

    Répondre
      1. Laurent

        Oups le package.json est erroné :) je le mets à jour sous peu! J’utilise « cookie » en 0.0.4 mais il semble que la lib soit vieille et qu’il faille utiliser « cookies » en 0.3.2 mais l’utilisation des cookie est différente :) bref avec cookie 0.0.4 dans les lib du package.json cela fonctionne mais il faudra migrer avec cookies. De même en testant ejs ne fonctionne plus de cette manière mais avec une nouvelle lib liée à express (pour express 3.x) je l’ai fait fonctionner donc je mets tout ça à jour ce soir ou demain!

        Répondre
    1. Will

      Génial! Merci à toi aussi Laurent!

      J’ai du faire un upgrade vers express3 et socket.io pour implémenter un upload de fichier avec les derniers tutos dispos, et du coup je me suis retrouvé avec les problèmes que tu soulèves… Je suis passé par des workaround bizares (https://groups.google.com/forum/?fromgroups=#!topic/express-js/qZwiwBsJZ04), mais grâce a toi, je retrouve quelquechose de propre

      Seule victime: ma session quand je suis en web app depuis iOS… des que je quitte la web app, le test


      if (req.session.username) {

      est systématiquement faux: username est toujours inexistant, du coup je dois me relogger a chaque fois que je relance l’app… mais comme la session existe toujours, je ne peux pas utiliser le même nom (post(‘login’))

      Je n’avais pas le probleme avant l’upgrade vers express 3 / socket.io 9, et je suis toujours en node 0.6.9. Aucun souci avec le reste des browsers (OS X: safari, chrome, firefox – iOS: safari, chrome – Androïd: chrome)
      En gros seul le mode web app iOS ne fonctionne plus.

      Quand je fais un console.log(req.session), j’ai ça:

      req.session:
      {
      "cookie": {
      "originalMaxAge": null,
      "expires": null,
      "httpOnly": true,
      "path": "/"
      }
      }

      la ou avant j’avais (sans doute, les traces viennent du reste de mes browsers pour lesquels tout marche bien navette)


      req.session:
      {
      "cookie": {
      "originalMaxAge": null,
      "expires": null,
      "httpOnly": true,
      "path": "/"
      },
      "username": "moi"
      }

      Il parait que c’est un probleme de cookie sans « expire » de spécifié, ce qui provoquerait sa suppression au lancement du browser sous iOS (http://stackoverflow.com/questions/7323546/session-expires-when-run-as-web-app)

      Any idea?

      Répondre
      1. Will

        Je confirme que j’ai bien au relaunch de la web app avec Express 2:


        req.session:
        {
        "cookie": {
        "originalMaxAge": null,
        "expires": null,
        "httpOnly": true,
        "path": "/"
        },
        "username": "moi"
        }

        Why the hell « username » disparaît au relaunch de la webapp (iOS) avec Express3? Il apparait que ce n’est pas lié à l’absence de propriété « expires » (ce n’est pas lié au browser puisque il ne bouge pas entre les version d’Express 2 et 3), donc mon dernier lien sur mon comm précédent se révèle être une fausse piste…

        Une piste que j’explore est dans post(‘login’):


        // Validate if username is free
        req.sessionStore.all(function (err, sessions) {
        //...
        if (err) {
        options.error = ""+err;
        res.render("login", options);
        } else {
        req.session.username = {uneStructureCommeCookieAvecLesChampsQuiVontBien};
        res.redirect("/");
        }
        });

        mais pour l’instant sans succes… :'(

        Pensez-vous que ca a du sens?

        Merci pour vos avis

        Répondre
        1. Will

          Good nouze everyone!!

          Le problème était bien lié au cookie, du moins à sa configuration par défaut qui change a priori entre Express2 et 3.
          Ca saute aux yeux (forcément, quand on regarde les bonnes traces +_+):


          {
          "cookie": {

          "originalMaxAge": 14400000,

          "expires": "2013-01-15T16:33:22.474Z",
          "httpOnly": true,
          "path": "/"
          },
          "username": "Will"
          }

          c’est ce « originalMaxAge » qui bouge par défaut.

          Dans les sources du tutos, sans config particulière dans


          this.use(express.session({
          // Private crypting key
          "secret": "some private string",
          // Internal session data storage engine, this is the default engine embedded with connect.
          // Much more can be found as external modules (Redis, Mongo, Mysql, file...). look at "npm search connect session store"
          "store": new express.session.MemoryStore({ reapInterval: 60000 * 10 })
          }));

          il est d’office positionné à cette valeur, et on le retrouve dans les traces ci dessus.

          En revanche, il faudra le faire à la main dans le cas de Express3 (et je reprends la les sources de Laurent, qui diffèrent assez peu au final):


          this.use(express.session({
          // Private crypting key
          "secret": "fuck",
          "store": this.sessionStore

          //FOR EXPRESS 3.x
          ,cookie: {
          maxAge: 14400000 //ou ce qui vous arrange
          }

          }));

          Sinon, on aura


          {
          "cookie": {

          "originalMaxAge": null,

          "expires": "2013-01-15T16:33:22.474Z",
          "httpOnly": true,
          "path": "/"
          },
          "username": "Will"
          }

          Et la piste de mon premier appel au secours était bien la bonne (http://stackoverflow.com/questions/7323546/session-expires-when-run-as-web-app), mais pas sur la bonne propriété :p (“maxAge » et pas « expires »)

          Side effect: avant, la distinction etait faite entre les sessions Safari et WebApp… ce n’est plus le cas. Un switch entre les 2 modes reviendra a la meme session. La vérité est ailleurs ?

          Répondre
  11. Laurent

    En bossant sur mon projet j’ai tout repris et en fait on s’en sort avec Express/Connect pour parser les cookies (aucun plugin pour les cookies), ça diffère un peu par contre dans les dernières versions :) je ferai la maj et referait un com, tu pourras « nettoyer » mes coms quand ce sera fait, désolé des multi com c’est pour la bonne cause (des exemples nodejs/express avec les dernières versions). Ton tutoriel a dû être suivi par pas mal de francophones donc c’est important :)

    Juste pour info, pour des authentifications le plugin everyauth http://everyauth.com/ est superbe, je me suis fait un chat avec accès facebook just for fun (pour éviter l’inscription/login du chat) c’est vraiment simple à mettre en place et on étend de la même manière twitter et bien d’autres services!

    ++ et encore merci!

    Répondre
  12. Ping : Node Js + Express Js + Jade + Socket I/O + Sails Js | Sois Net !

  13. sshenron

    Salut,

    Merci pour ce tutu qui m’a bien été utile !!!
    J’ai donc réussi à faire ma socket qui peut lire le contenu de la session. Mais pas la modifier c’est normal ?

    //Etape 1 Initialisation de la session 
    app.get('/', function(req, res){
    	req.session.user = {
    		pseudo: 'invité'
    	};
    });
     
    // Etape 2 lecture depuis socket io et modification
    sessionStore.get(sessionID, function (err, session) {
    	console.log(session.user.pseudo); //invité
    	sesion.user.pseudo = 'thomas'; //Je tente d'écrire dans la session
    	...
     
    // Etape 3 lecture depuis une reoute express
    app.get('/test', function(req, res){
    	console.log(req.session.user.pseudo); 	//invité
    });

    Une petite idée ? Ce n’est peut être tout simplement pas possible …

    Répondre
    1. naholyr Auteur de l’article

      Le fait de se contenter d’écrire dans l’objet « session » marche quand on est dans une route Express parce que derrière il y a le middleware session qui va enregistrer l’objet dans le moteur de stockage des sessions.

      Ici tu n’as pas ça, tu as juste l’accès à ce moteur de stockage. Il faut donc passer par la méthode « set » sur le modèle de la méthode « get » pour la lecture.

      Répondre
      1. sshenron

        Ca fonctionne !!!!!!!! Merci beaucoup.

        session.user.email = ‘test@test.com';

        //On met a jour la session
        sessionStore.set(sessionID, session, function (err) {
        if(err)
        Y.log(‘err write session’);
        });

        Répondre

Laisser un commentaire