Note: I'm migrating from gonzalo123.com to here. When I finish I'll swap the DNS to here. The "official" blog will be always gonzalo123.com

      Integrating WebSockets with PHP applications. Silex and socket.io playing together.

      WebSockets are great. We can start a persistent connection from our browser to our server and use this connection to send real time notifications to our users. Normally when we integrate WebSockets with an existing Web application, we need to face with one slight problem. Our Web application runs on a Web server (imagine, for example one Silex application). We can use a login form and ensure all requests are authorized (using a security layer). This problem is solved years ago. We can use Basic HTTP authentification, Digtest authentification, a session based authentication, token based authentificatio, OAuth, … The problem arrives when we add WebSocket server. WebSocket server is another serve. We can use node.js, ruby, or even PHP with Rachet. But how we can ensure that WebSocket server’s requests are also authenticated? We can try to share our authentification provider between both servers, but this solution is quite “exotic”. That was the idea behind my blog post: post some time ago. I’ve been thinkin a lot about it, and also read posts and speak with colleages about this subject. Finally I’m using the following solution. Let me explain it.

      Websockets are bi-directional. We can get messages in the browser and send them from browser to server. Basically the solution is to disable the messages from the browser to the server via WebSockets. In fact HTML5 provides another tool to do that called Server Side Events (aka SSE), but SSE aren’t as widely used as WebSockets. Because of that I preffer to use WebSockets (without using the browser-to-server chanel) instead of SSE.

      Let’s create a simple Silex application:

      class Application extends Silex\Application
      {
          use Silex\Application\TwigTrait;
      }
       
      $app = new Application();
       
      $app->register(new Silex\Provider\TwigServiceProvider(), array(
          'twig.path' => __DIR__ . '/../views',
      ));
       
      $app->get('/', function () use ($app) {
          return $app->render('home.twig');
      });
       
      $app->run();
      

      And our main template with html file

      <!DOCTYPE html>
      <html>
      <head>
          <title></title>
      </head>
      <body>
      <script src="//localhost:8080/socket.io/socket.io.js"></script>
      <script>
          var socket = io.connect('//localhost:8080');
       
          socket.on('id1', function (data) {
              console.log("mensage from websocket: " + data);
          });
      </script>
      </body>
      </html>
      

      Now we have Silex application that connects to a WebSockets server. I will use socket.io to build the WebSocket server:

      var CONF = {
              IO: {HOST: '0.0.0.0', PORT: 8080}
          },
          io = require('socket.io').listen(CONF.IO.PORT, CONF.IO.HOST);
      

      Whit this ultra minimal configuration we can connect from Silex application to WebSocket server and our web application will listen to messages marked as’id1’ from the WebSocket server but, how can we do to send messages? As I said before we only rely on Silex application (in this example there isn’t any security layer, but we can use our custom login). The trick is to create a new server within our node.js server. Start this server at localhost and perform a curl request from our Silex Application to our node.js server to send the WebSockets push notifications. The idea is:

      • User clicks a link in our html (generated by our Silex application)
      • This request is a standard Silex request (using our security layer)
      • Then Silex performs a curl request to node.js server.
      • If our Silex application and node.js application are in the same server we will create a new server at localhost. In this example we are going to use Express to do that.
      • Express server will handle requests from our Silex application (not from any other host) and will send WebSocket messages

      Now our node.js application will change to

      var CONF = {
              IO: {HOST: '0.0.0.0', PORT: 8080},
              EXPRESS: {HOST: 'localhost', PORT: 26300}
          },
          io = require('socket.io').listen(CONF.IO.PORT, CONF.IO.HOST),
          app = require('express')();
       
      app.get('/emit/:id/:message', function (req, res) {
          io.sockets.emit(req.params.id, req.params.message);
          res.json('OK');
      });
       
      app.listen(CONF.EXPRESS.PORT, CONF.EXPRESS.HOST);
      

      And our html template will change to (I will use Zepto to perform AJAX requests):

      <!DOCTYPE html>
      <html>
      <head>
          <title></title>
      </head>
      <body>
      <ul>
          <li><a href="#" onclick="emit('id1', 'hello')">emit('id1', 'hello')</a></li>
          <li><a href="#" onclick="emit('id1', 'bye')">emit('id1', 'bye')</a></li>
      </ul>
      <script src="//localhost:8080/socket.io/socket.io.js"></script>
      <script src="//cdnjs.cloudflare.com/ajax/libs/zepto/1.1.1/zepto.min.js"></script>
      <script>
          var socket = io.connect('//localhost:8080');
       
          socket.on('id1', function (data) {
              console.log("mensage from websocket: " + data);
          });
       
          function emit(id, message) {
              $.get('/emit/' + id +  '/' + message);
          }
      </script>
      </body>
      </html>
      

      Now we need to add another route to our Silex application

      use Symfony\Component\HttpFoundation\Response;
       
      $app->get('/emit/{id}/{message}', function ($id, $message) use ($app) {
          $s = curl_init();
          curl_setopt($s, CURLOPT_URL, "http://localhost:26300/emit/{$id}/{$message}");
          curl_setopt($s, CURLOPT_RETURNTRANSFER, true);
          $content = curl_exec($s);
          $status = curl_getinfo($s, CURLINFO_HTTP_CODE);
          curl_close($s);
       
          return new Response($content, $status);
      });
      

      And that’s all. Our Request from Silex arrives to WebSocket emmiter using a “secure” layer. OK, now you can said: yes, but anybody can connect to the WebSocket server and listen to ‘id1’ chanel, without any restriction. Yes, it’s true. But here you can use different solutions to ensure privacy. For example you can use a “non-obvious” chanel name based on cryptografic funcions. It’s not 100% secure, but it’s the same security layer than the standard session based security mechanism. If we know the cookie name we can perform a session hijacking attack and gain access to secure areas (without knowing the login credentials). We can generate chanel names like this: 7265cfe8fe3daa4c5069d609a0312dd2 with our Silex Application and send to the browser with an AJAX request.

      I’ve created an small screencast to see the prototype in action. (source code in my github account) In the screencast we can see how to install the prototype from github, install PHP’s vendors and the node js modules. We also can see how websocket works with two browser instances, and how to send messages directly accesing to Express application using localhost interface (and an error when I try to reach to Express server using a different network interface)

      youtube

      What do you think? Do you have another solution?

      comments powered by Disqus