Ratchet PHP WAMP - React / ZeroMQ-diffusion spécifique à l'utilisateur

Remarque:: de la même façon que cette question qui utilise MessageComponentInterface. Je suis à l'aide de WampServerInterface au lieu de cela, donc cette question se rapporte spécifiquement à cette partie. J'ai besoin d'une réponse avec des exemples de codes et une explication, car je peux voir que cela sera utile à d'autres à l'avenir.

Tenter boucle pousse pour les utilisateurs individuels

j'utilise la partie WAMP de Ratchet et ZeroMQ, et j'utilise actuellement avoir une version de travail de l' pousser l'intégration tutoriel.

je suis d'essayer d'effectuer les opérations suivantes:

  • le serveur zeromq est opérationnel, prêt à enregistrer les abonnés et les désabonneurs
  • un utilisateur se connecte dans son navigateur via le protocole websocket
  • boucle est démarré ce qui envoie des données à spécifiques de l'utilisateur qui en fait la demande
  • Lorsque l'utilisateur se déconnecte, le boucle pour les données de cet utilisateur est arrêté

j'ai des points (1) et (2) de travail, cependant, le problème que j'ai est avec le troisième:

tout d'Abord: Comment puis-je envoyer des données spécifiques à chaque utilisateur? la diffusion l'envoie à tout le monde, à moins que peut-être les "sujets" finissent par être des identificateurs d'utilisateur individuels peut-être?

Secundo:j'ai un gros problème de sécurité. si j'envoie quel identifiant d'utilisateur veut s'abonner du côté client, qu'il il semble que je dois, alors l'utilisateur pourrait juste changer la variable à L'ID D'un autre utilisateur et leurs données sont retournés à la place.

Troisièmement: je dois lancer un script php séparé contenant le code de zeromq pour lancer la boucle réelle. Je ne suis pas sûr que ce soit la meilleure façon de faire cela et je préférerais que cela fonctionne complètement dans la base de code plutôt que dans un fichier php séparé. C'est un domaine important que je dois trier.

le code suivant montre ce que j'ai actuellement.

le serveur qui s'exécute à partir de la console

je tape littéralement php bin/push-server.php pour exécuter cette. Les souscriptions et les non-souscriptions sont sorties sur ce terminal à des fins de débogage.

$loop   = ReactEventLoopFactory::create();
$pusher = Pusher;

$context = new ReactZMQContext($loop);
$pull = $context->getSocket(ZMQ::SOCKET_PULL);
$pull->bind('tcp://127.0.0.1:5555');
$pull->on('message', array($pusher, 'onMessage'));

$webSock = new ReactSocketServer($loop);
$webSock->listen(8080, '0.0.0.0'); // Binding to 0.0.0.0 means remotes can connect
$webServer = new RatchetServerIoServer(
    new RatchetWebSocketWsServer(
        new RatchetWampWampServer(
            $pusher
        )
    ),
    $webSock
);

$loop->run();

le Pusher qui envoie des données sur des websockets

j'ai omis les choses inutiles et concentré sur le onMessage() et onSubscribe() méthodes.

public function onSubscribe(ConnectionInterface $conn, $topic) 
{
    $subject = $topic->getId();
    $ip = $conn->remoteAddress;

    if (!array_key_exists($subject, $this->subscribedTopics)) 
    {
        $this->subscribedTopics[$subject] = $topic;
    }

    $this->clients[] = $conn->resourceId;

    echo sprintf("New Connection: %s" . PHP_EOL, $conn->remoteAddress);
}

public function onMessage($entry) {
    $entryData = json_decode($entry, true);

    var_dump($entryData);

    if (!array_key_exists($entryData['topic'], $this->subscribedTopics)) {
        return;
    }

    $topic = $this->subscribedTopics[$entryData['topic']];

    // This sends out everything to multiple users, not what I want!!
    // I can't send() to individual connections from here I don't think :S
    $topic->broadcast($entryData);
}

le script pour commencer à utiliser le code Pusher ci-dessus dans un boucle

C'est mon problème - il s'agit d'un fichier php séparé qui, je l'espère, pourra être intégré dans d'autres codes à l'avenir, mais actuellement Je ne suis pas sûr de savoir comment l'utiliser correctement. Est-ce que j'attrape l'ID de l'utilisateur de la session? J'ai encore besoin de l'envoyer du côté client...

// Thought sessions might work here but they don't work for subscription
session_start();
$userId = $_SESSION['userId'];

$loop   = ReactEventLoopFactory::create();

$context = new ZMQContext();
$socket = $context->getSocket(ZMQ::SOCKET_PUSH, 'my pusher');
$socket->connect("tcp://localhost:5555");

$i = 0;
$loop->addPeriodicTimer(4, function() use ($socket, $loop, $userId, &$i) {

   $entryData = array(
       'topic'     => 'subscriptionTopicHere',
       'userId'    => $userId
    );
    $i++;

    // So it doesn't go on infinitely if run from browser
    if ($i >= 3)
    {
        $loop->stop();
    }

    // Send stuff to the queue
    $socket->send(json_encode($entryData));
});

enfin, les js côté client à souscrire avec

$(document).ready(function() { 

    var conn = new ab.Session(
        'ws://localhost:8080' 
      , function() {            
            conn.subscribe('topicHere', function(topic, data) {
                console.log(topic);
                console.log(data);
            });
        }
      , function() {          
            console.warn('WebSocket connection closed');
        }
      , {                       
            'skipSubprotocolCheck': true
        }
    );
});

Conclusion

Le dessus est le travail, mais j'ai vraiment besoin de comprendre à la suivantes:

  • Comment puis-je envoyer des messages à des utilisateurs individuels? Quand ils visitent la page qui démarre la connexion websocket dans JS, devrais-je aussi commencer le script qui enfonce des trucs dans la file D'attente en PHP (le zeromq)? C'est ce que je fais manuellement, et c'est juste se sent mal.

  • lors de l'abonnement d'un utilisateur de JS, il ne peut pas être sûr de saisir l'id de l'utilisateur de la session et de l'Envoyer à partir de côté client. Cela pourrait être faux. S'il vous plaît dites-moi il y a un moyen plus facile, et si oui, comment?

17
demandé sur Community 2013-09-28 14:16:41

2 réponses

Remarque: Ma réponse ici ne pas incluez des références à ZeroMQ, car je ne l'utilise plus. Cependant, je suis sûr que vous serez en mesure de comprendre comment utiliser ZeroMQ avec cette réponse si vous avez besoin.

utilisez JSON

d'Abord et avant tout, le Websocket RFC et Wamp Spec préciser que le sujet auquel s'abonner doit être un chaîne. Je triche un peu ici, mais je suis toujours fidèle à la spécification: je passe JSON à travers à la place.

{
    "topic": "subject here",
    "userId": "1",
    "token": "dsah9273bui3f92h3r83f82h3"
}

JSON est toujours une chaîne, mais il me permet de passer à travers plus de données à la place du "topic", et il est simple pour PHP de faire un json_decode() sur l'autre extrémité. Bien sûr, vous devriez valider que vous recevez réellement JSON, mais c'est à votre mise en œuvre.

alors qu'est-ce que je traverse ici, et pourquoi?

  • thème

le sujet est Le sous réserve que l'utilisateur s'abonne à. Vous utilisez ceci pour décider quelles données vous renvoyez à l'utilisateur.

  • nom d'utilisateur

évidemment L'ID de l'utilisateur. Vous vérifier que cet utilisateur existe et est autorisé à souscrire, à l'aide de la partie suivante:

  • Jeton

Ce doit être un utilisation jeton généré au hasard, généré dans votre PHP, et passé à un Variable JavaScript. Quand je dis "une utilisation", je veux dire chaque fois que vous rechargez la page (et, par extension, sur chaque requête HTTP), votre variable JavaScript devrait avoir un nouveau token. Ce token doit être stocké dans la base de données contre L'ID de l'utilisateur.

ensuite, une fois qu'une requête websocket est faite, vous faites correspondre le token et l'identifiant de l'utilisateur à ceux de la base de données pour s'assurer que l'utilisateur est bien qui ils disent qu'ils sont, et qu'ils n'ont pas joué avec le JS variable.

Remarque: Dans votre gestionnaire d'événements, vous pouvez utiliser $conn->remoteAddress pour obtenir L'adresse IP de la connexion, donc si quelqu'un essaie de se connecter de manière malveillante, vous pouvez le bloquer (le log ou quelque chose).

Pourquoi ce travail?

Cela fonctionne parce que chaque fois qu'une nouvelle connexion à l'entrée, et le jeton unique garantit qu'Aucun utilisateur n'aura accès aux données d'abonnement de quiconque.

le Le serveur

voici ce que j'utilise pour exécuter la boucle et le gestionnaire d'événements. Je suis en train de créer la boucle, de faire toute la création d'objets de style décorateur, et de passer dans mon EventHandler (que je viendrai bientôt) avec la boucle là aussi.

$loop = Factory::create();

new IoServer(
    new WsServer(
        new WampServer(
            new EventHandler($loop) // This is my class. Pass in the loop!
        )
    ),
    $webSock
);

$loop->run();

Le Gestionnaire D'Événement

class EventHandler implements WampServerInterface, MessageComponentInterface
{
    /**
     * @var \React\EventLoop\LoopInterface
     */
    private $loop;

    /**
     * @var array List of connected clients
     */
    private $clients;

    /**
     * Pass in the react event loop here
     */
    public function __construct(LoopInterface $loop)
    {
        $this->loop = $loop;
    }

    /**
     * A user connects, we store the connection by the unique resource id
     */
    public function onOpen(ConnectionInterface $conn)
    {
        $this->clients[$conn->resourceId]['conn'] = $conn;
    }

    /**
     * A user subscribes. The JSON is in $subscription->getId()
     */
    public function onSubscribe(ConnectionInterface $conn, $subscription)
    {
        // This is the JSON passed in from your JavaScript
        // Obviously you need to validate it's JSON and expected data etc...
        $data = json_decode(subscription->getId());

        // Validate the users id and token together against the db values

        // Now, let's subscribe this user only
        // 5 = the interval, in seconds
        $timer = $this->loop->addPeriodicTimer(5, function() use ($subscription) {
            $data = "whatever data you want to broadcast";
            return $subscription->broadcast(json_encode($data));
        });

        // Store the timer against that user's connection resource Id
        $this->clients[$conn->resourceId]['timer'] = $timer;
    }

    public function onClose(ConnectionInterface $conn)
    {
        // There might be a connection without a timer
        // So make sure there is one before trying to cancel it!
        if (isset($this->clients[$conn->resourceId]['timer']))
        {
            if ($this->clients[$conn->resourceId]['timer'] instanceof TimerInterface)
            {
                $this->loop->cancelTimer($this->clients[$conn->resourceId]['timer']);
            }
        }

        unset($this->clients[$conn->resourceId]);
    }

    /** Implement all the extra methods the interfaces say that you must use **/
}

c'est en gros ça. Les principaux points sont ici:

  • jeton Unique, id utilisateur et id de connexion de fournir la combinaison unique nécessaires pour s'assurer qu'un utilisateur Je ne vois pas les données d'un autre utilisateur.
  • token Unique signifie que si le même utilisateur ouvre une autre page et demande de s'abonner, il aura son propre combo d'id de connexion + token de sorte que le même utilisateur n'aura pas le double des abonnements sur la même page (essentiellement, chaque connexion a ses propres données individuelles).

Extension

vous devriez vous assurer que toutes les données sont validées et pas une tentative de piratage avant de faire quoi que ce soit avec elle. Enregistrer toutes les tentatives de connexion utiliser quelque chose comme Monolog, et mettre en place le renvoi d'e-mail si une situation critique se produit (comme le serveur s'arrête de fonctionner parce que quelqu'un est un bâtard et tente de pirater votre serveur).

Points De Fermeture

  • Validez Tout. Je ne peux pas le souligner assez. Votre jeton unique qui change chaque demande est important.
  • rappelez-vous, si vous re-générez le token sur chaque requête HTTP, et que vous faites une requête POST avant d'essayer de vous connecter via websockets, vous devrez transmettre le token généré à votre JavaScript avant d'essayer de vous connecter (sinon votre token sera invalide).
  • Journal tout. Tenez un registre de toutes les personnes qui se connectent, qui demandent quel sujet et qui se déconnectent. Monolog est parfait pour ça.
20
répondu Jimbo 2015-04-29 09:26:24

pour envoyer à des utilisateurs spécifiques, vous avez besoin d'un modèle de routeur-DEALER au lieu de PUB-SUB. Cela est expliqué dans le Guide, au chapitre 3. Sécurité, si vous utilisez ZMQ v4.0, est manipulé au niveau du fil, donc vous ne le voyez pas dans l'application. Il nécessite encore du travail, à moins que vous n'utilisiez la liaison CZMQ, qui fournit un cadre d'authentification (zauth).

en gros, pour s'authentifier, vous installez un handler sur inproc://zeromq.ZAP.01, et de répondre à des requêtes sur ce socket. Google ZeroMQ ZAP pour la RFC; il y a aussi un cas de test dans le libzmq de base/tests/test_security_curve.programme cpp.

2
répondu Pieter Hintjens 2013-09-29 17:54:37