├── .gitignore ├── README.md ├── composer.json ├── examples ├── chat.html ├── chat.php ├── democert.pem ├── echo_client.php ├── headers.html ├── headers.php ├── remoteEvents.html ├── remoteEvents.php ├── rooms.html ├── rooms.php ├── ssl_echo.html ├── ssl_echo.php ├── tcp_proxy_example.html ├── tcp_proxy_example.php ├── time.html └── time.php ├── js ├── bower.json └── phpws.js ├── src └── Devristo │ └── Phpws │ ├── Client │ ├── Connector.php │ └── WebSocket.php │ ├── Exceptions │ ├── WebSocketFrameSizeMismatch.php │ ├── WebSocketInvalidChallengeResponse.php │ ├── WebSocketInvalidKeyException.php │ ├── WebSocketInvalidUrlScheme.php │ └── WebSocketMessageNotFinalised.php │ ├── Framing │ ├── WebSocketFrame.php │ ├── WebSocketFrame76.php │ ├── WebSocketFrameInterface.php │ └── WebSocketOpcode.php │ ├── Messaging │ ├── MessageInterface.php │ ├── RemoteEventMessage.php │ ├── WebSocketMessage.php │ ├── WebSocketMessage76.php │ └── WebSocketMessageInterface.php │ ├── Protocol │ ├── Handshake.php │ ├── StackTransport.php │ ├── TransportInterface.php │ ├── WebSocketConnection.php │ ├── WebSocketTransport.php │ ├── WebSocketTransportFactory.php │ ├── WebSocketTransportFlash.php │ ├── WebSocketTransportHixie.php │ ├── WebSocketTransportHybi.php │ ├── WebSocketTransportInterface.php │ └── WebsocketTransportRole.php │ ├── Reflection │ └── FullAccessWrapper.php │ ├── RemoteEvent │ ├── RemoteEventTransport.php │ ├── RemoteEvents.php │ └── Room.php │ └── Server │ ├── OriginEnforcer.php │ ├── UriHandler │ ├── ClientRouter.php │ ├── WebSocketUriHandler.php │ └── WebSocketUriHandlerInterface.php │ └── WebSocketServer.php └── tests ├── framing.test.php └── server.test.php /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /phpunit.phar 3 | /vendor 4 | /composer.lock 5 | /composer.phar -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | WebSocket Server and Client library for PHP. Works with the latest HyBi specifications, as well the older Hixie #76 specification used by older Chrome versions and some Flash fallback solutions. 2 | 3 | This project was started to bring more interactive features to http://www.u2start.com/ 4 | 5 | Features 6 | ============ 7 | Server 8 | * Hixie #76 and Hybi #12 protocol versions 9 | * Flash client support (also serves XML policy file on the same port) 10 | * See https://github.com/gimite/web-socket-js for a compatible Flash Client 11 | * Native Firefox, Safari (iPod / iPhone as well), Chrome and IE10 support. With Flash Client every browser supporting Flash works as well (including IE6-9, Opera, Android and other older desktop browsers). 12 | * Opera (Mobile) supports WebSockets natively but support has been disabled by default. Can be enabled in opera:config. 13 | 14 | Client 15 | * Hybi / Hixie76 support. 16 | * Event-based Async I/O 17 | 18 | 19 | Getting started 20 | ================= 21 | The easiest way to set up PHPWS is by using it as Composer dependency. Add the following to your composer.json 22 | 23 | ```json 24 | { 25 | "repositories": [ 26 | { 27 | "type": "vcs", 28 | "url": "https://github.com/Devristo/phpws" 29 | } 30 | ], 31 | "require": { 32 | "devristo/phpws": "dev-master" 33 | } 34 | } 35 | ``` 36 | 37 | And run ```php composer.phar install``` 38 | 39 | To verify it is working create a time.php in your project root 40 | ```php 41 | require_once("vendor/autoload.php"); 42 | use Devristo\Phpws\Server\WebSocketServer; 43 | 44 | $loop = \React\EventLoop\Factory::create(); 45 | 46 | // Create a logger which writes everything to the STDOUT 47 | $logger = new \Zend\Log\Logger(); 48 | $writer = new Zend\Log\Writer\Stream("php://output"); 49 | $logger->addWriter($writer); 50 | 51 | // Create a WebSocket server using SSL 52 | $server = new WebSocketServer("tcp://0.0.0.0:12345", $loop, $logger); 53 | 54 | $loop->addPeriodicTimer(0.5, function() use($server, $logger){ 55 | $time = new DateTime(); 56 | $string = $time->format("Y-m-d H:i:s"); 57 | $logger->notice("Broadcasting time to all clients: $string"); 58 | foreach($server->getConnections() as $client) 59 | $client->sendString($string); 60 | }); 61 | 62 | 63 | // Bind the server 64 | $server->bind(); 65 | 66 | // Start the event loop 67 | $loop->run(); 68 | ``` 69 | 70 | And a client time.html as follows 71 | ```html 72 | 73 | 74 | WebSocket TEST 75 | 76 | 77 |

Server Time

78 | 79 | 80 | 86 | 87 | 88 | ``` 89 | Now run the time.php from the command line and open time.html in your browser. You should see the current time, broadcasted 90 | by phpws at regular intervals. If this works you might be interested in more complicated servers in the examples folder. 91 | 92 | Getting started with the Phpws Client 93 | ======================================= 94 | The following is a client for the websocket server hosted at http://echo.websocket.org 95 | 96 | ```php 97 | require_once("vendor/autoload.php"); // Composer autoloader 98 | 99 | $loop = \React\EventLoop\Factory::create(); 100 | 101 | $logger = new \Zend\Log\Logger(); 102 | $writer = new Zend\Log\Writer\Stream("php://output"); 103 | $logger->addWriter($writer); 104 | 105 | $client = new \Devristo\Phpws\Client\WebSocket("ws://echo.websocket.org/?encoding=text", $loop, $logger); 106 | 107 | $client->on("request", function($headers) use ($logger){ 108 | $logger->notice("Request object created!"); 109 | }); 110 | 111 | $client->on("handshake", function() use ($logger) { 112 | $logger->notice("Handshake received!"); 113 | }); 114 | 115 | $client->on("connect", function($headers) use ($logger, $client){ 116 | $logger->notice("Connected!"); 117 | $client->send("Hello world!"); 118 | }); 119 | 120 | $client->on("message", function($message) use ($client, $logger){ 121 | $logger->notice("Got message: ".$message->getData()); 122 | $client->close(); 123 | }); 124 | 125 | $client->open(); 126 | $loop->run(); 127 | ``` 128 | 129 | 130 | Known Issues 131 | ================== 132 | * Lacks ORIGIN checking (can be implemented manually in onConnect using getHeaders(), just disconnect the user when you dont like the Origin header) 133 | * No support for extension data from the HyBi specs. 134 | 135 | Requirements 136 | ================= 137 | *Server* 138 | * PHP 5.4 139 | * Open port for the server 140 | * PHP OpenSSL module to run a server over a encrypted connection 141 | * http://pecl.php.net/package/pecl_http as its a dependency of Zend\Uri 142 | 143 | * Composer dependencies * 144 | These will be installed automatically when using phpws as a composer package. 145 | 146 | * Reactphp 147 | * ZF2 Logger 148 | 149 | *Client* 150 | * PHP 5.4 151 | * Server that implements the HyBi (#8-#12) draft version 152 | * PHP OpenSSL module to connect using SSL (wss:// uris) 153 | 154 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mpociot/phpws", 3 | "description": "WebSocket Server and Client library for PHP", 4 | "minimum-stability": "stable", 5 | "authors": [ 6 | { 7 | "name": "Devristo", 8 | "email": "chris@devristo.com" 9 | } 10 | ], 11 | "autoload": { 12 | "psr-0": { 13 | "Devristo\\Phpws\\": "src/" 14 | } 15 | }, 16 | "require": { 17 | "zendframework/zend-log": "2.*", 18 | "react/socket": "^1.0 || ^0.8.6", 19 | "react/stream": "^1.0 || ^0.7.5", 20 | "zendframework/zend-http": "2.*" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/chat.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Simple Chat 4 | 5 | 10 | 11 | 70 | 71 | 72 | 73 |

WebSocket Test

74 |
75 | 76 | 77 | 78 |
Server will echo your response!
79 | 80 | -------------------------------------------------------------------------------- /examples/chat.php: -------------------------------------------------------------------------------- 1 | #!/php -q 2 | php chat.php 11 | use Devristo\Phpws\Framing\WebSocketFrame; 12 | use Devristo\Phpws\Framing\WebSocketOpcode; 13 | use Devristo\Phpws\Messaging\WebSocketMessageInterface; 14 | use Devristo\Phpws\Protocol\WebSocketTransportInterface; 15 | use Devristo\Phpws\Server\IWebSocketServerObserver; 16 | use Devristo\Phpws\Server\UriHandler\WebSocketUriHandler; 17 | use Devristo\Phpws\Server\WebSocketServer; 18 | 19 | /** 20 | * This ChatHandler handler below will respond to all messages sent to /chat (e.g. ws://localhost:12345/chat) 21 | */ 22 | class ChatHandler extends WebSocketUriHandler { 23 | 24 | /** 25 | * Notify everyone when a user has joined the chat 26 | * 27 | * @param WebSocketTransportInterface $user 28 | */ 29 | public function onConnect(WebSocketTransportInterface $user){ 30 | foreach($this->getConnections() as $client){ 31 | $client->sendString("User {$user->getId()} joined the chat: "); 32 | } 33 | } 34 | 35 | /** 36 | * Broadcast messages sent by a user to everyone in the room 37 | * 38 | * @param WebSocketTransportInterface $user 39 | * @param WebSocketMessageInterface $msg 40 | */ 41 | public function onMessage(WebSocketTransportInterface $user, WebSocketMessageInterface $msg) { 42 | $this->logger->notice("Broadcasting " . strlen($msg->getData()) . " bytes"); 43 | 44 | foreach($this->getConnections() as $client){ 45 | $client->sendString("User {$user->getId()} said: ".$msg->getData()); 46 | } 47 | } 48 | } 49 | class ChatHandlerForUnroutedUrls extends WebSocketUriHandler { 50 | /** 51 | * This class deals with users who are not routed 52 | */ 53 | public function onConnect(WebSocketTransportInterface $user){ 54 | //do nothing 55 | $this->logger->notice("User {$user->getId()} did not join any room"); 56 | } 57 | public function onMessage(WebSocketTransportInterface $user, WebSocketMessageInterface $msg) { 58 | //do nothing 59 | $this->logger->notice("User {$user->getId()} is not in a room but tried to say: {$msg->getData()}"); 60 | } 61 | } 62 | 63 | 64 | $loop = \React\EventLoop\Factory::create(); 65 | 66 | // Create a logger which writes everything to the STDOUT 67 | $logger = new \Zend\Log\Logger(); 68 | $writer = new Zend\Log\Writer\Stream("php://output"); 69 | $logger->addWriter($writer); 70 | 71 | // Create a WebSocket server 72 | $server = new WebSocketServer("tcp://0.0.0.0:12345", $loop, $logger); 73 | 74 | // Create a router which transfers all /chat connections to the ChatHandler class 75 | $router = new \Devristo\Phpws\Server\UriHandler\ClientRouter($server, $logger); 76 | // route /chat url 77 | $router->addRoute('#^/chat$#i', new ChatHandler($logger)); 78 | // route unmatched urls durring this demo to avoid errors 79 | $router->addRoute('#^(.*)$#i', new ChatHandlerForUnroutedUrls($logger)); 80 | 81 | // Bind the server 82 | $server->bind(); 83 | 84 | // Start the event loop 85 | $loop->run(); 86 | 87 | ?> 88 | -------------------------------------------------------------------------------- /examples/democert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDwDCCAymgAwIBAgIBADANBgkqhkiG9w0BAQQFADCBojELMAkGA1UEBhMCVVMx 3 | DjAMBgNVBAgTBVRleGFzMQ8wDQYDVQQHEwZBdXN0aW4xHzAdBgNVBAoTFlBIUFdT 4 | IERlbW8gQ2VydGlmaWNhdGUxHzAdBgNVBAsTFlBIUFdTIERlbW8gQ2VydGlmaWNh 5 | dGUxDjAMBgNVBAMTBXBocHdzMSAwHgYJKoZIhvcNAQkBFhFwaHB3c0BleGFtcGxl 6 | LmNvbTAeFw0xMTEyMTgxNzI3MjlaFw0xMjEyMTcxNzI3MjlaMIGiMQswCQYDVQQG 7 | EwJVUzEOMAwGA1UECBMFVGV4YXMxDzANBgNVBAcTBkF1c3RpbjEfMB0GA1UEChMW 8 | UEhQV1MgRGVtbyBDZXJ0aWZpY2F0ZTEfMB0GA1UECxMWUEhQV1MgRGVtbyBDZXJ0 9 | aWZpY2F0ZTEOMAwGA1UEAxMFcGhwd3MxIDAeBgkqhkiG9w0BCQEWEXBocHdzQGV4 10 | YW1wbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDeht+8x3R4eCft 11 | yf5pHFJwCUB1zYuNrKULIS7XAFPzNHkTQZxU2BEBHjKdcSXJqXUMyByHH1bu70dw 12 | wjnUcXiMfJuTvtTkqyq2GbQu47SujqkcbguBUajo4+OgypmV7BDmatq/JJOH1wMQ 13 | NKmVE7in6v2fEp5DS9Q0DIeb0X0oZQIDAQABo4IBAjCB/zAdBgNVHQ4EFgQUS0a7 14 | Agqixx6D0ExIBwT2oSygZBswgc8GA1UdIwSBxzCBxIAUS0a7Agqixx6D0ExIBwT2 15 | oSygZBuhgaikgaUwgaIxCzAJBgNVBAYTAlVTMQ4wDAYDVQQIEwVUZXhhczEPMA0G 16 | A1UEBxMGQXVzdGluMR8wHQYDVQQKExZQSFBXUyBEZW1vIENlcnRpZmljYXRlMR8w 17 | HQYDVQQLExZQSFBXUyBEZW1vIENlcnRpZmljYXRlMQ4wDAYDVQQDEwVwaHB3czEg 18 | MB4GCSqGSIb3DQEJARYRcGhwd3NAZXhhbXBsZS5jb22CAQAwDAYDVR0TBAUwAwEB 19 | /zANBgkqhkiG9w0BAQQFAAOBgQAOuG4cfsbLq1CSxtLuhLCtx4XmEbGNIq6PRFbA 20 | +mnVEAs0J4rfeCDxNhQeSeZGduqE5jdq8kNFSBfu3vVYeFbNj91uObAc4ZUQeVgT 21 | N5zrGKKATHAwnftCoMloS07D82z0+HpBbPB5kJ4I6+Z8Dt65cS8aWzw2LE7Thcjg 22 | NXluZw== 23 | -----END CERTIFICATE----- 24 | -----BEGIN RSA PRIVATE KEY----- 25 | MIICXwIBAAKBgQDeht+8x3R4eCftyf5pHFJwCUB1zYuNrKULIS7XAFPzNHkTQZxU 26 | 2BEBHjKdcSXJqXUMyByHH1bu70dwwjnUcXiMfJuTvtTkqyq2GbQu47SujqkcbguB 27 | Uajo4+OgypmV7BDmatq/JJOH1wMQNKmVE7in6v2fEp5DS9Q0DIeb0X0oZQIDAQAB 28 | AoGBANcNsZxXhhAGz0/XLq+WV3U++7TdeEjq2HXxE7tk7bzUsU4S0mqMhaJ29KOD 29 | felug1he7HMJrpIrXPd0PT86iiwtfdzFKRjhDmKLWn8iSXp04tJOOY51BTmN/wO/ 30 | NCKnj60lbjwCjbGxGLq7VyYQjpnIVZ5Y/RGlc155eMBz0jkBAkEA94UfBWNxkNJA 31 | GQdXtrfF/ShbHK5HCgrh+oSRUQ7oqigTjod/1cXUmLWDX03oxELeKOiJx3nOi1YA 32 | 8L1BuBOLeQJBAOYmjMUPapcIvUubweEw6sYXaM+3Msie+HHTI0VzIQkvAUYUtGQz 33 | 5T0pUaSe9SGOpQeCUViczE5ko4T7GyfrnU0CQQCFl8gCdIXbEF+gIqJo8A9gb+Od 34 | O0MEXJNTTzHPeiiBjlff2apZiwkP0wgw7C/xndWiZr/WdhvQgH7JcJyD6aihAkEA 35 | xfnNR8pOH2PWKc7vRT4mBoamk198oNUW1BsSkTBK77JufxFaZ4O4oxcC8wAFz3r7 36 | /Oyd+wLOQHUTsFWs83cbVQJBAIX03j/CO2NuoEESTXWmE3MjMdghT45CKpfqHrFY 37 | T8kZ6oQtxtNibxC5dwWnl4pCGTDGuCkE7/tfLchs0lID16M= 38 | -----END RSA PRIVATE KEY----- 39 | -------------------------------------------------------------------------------- /examples/echo_client.php: -------------------------------------------------------------------------------- 1 | addWriter($writer); 17 | 18 | $client = new \Devristo\Phpws\Client\WebSocket("ws://echo.websocket.org/?encoding=text", $loop, $logger); 19 | //$client = new \Devristo\Phpws\Client\WebSocket("ws://google.com", $loop, $logger); 20 | $client->on("connect", function() use ($logger, $client){ 21 | $logger->notice("Or we can use the connect event!"); 22 | $client->send("Hello world!"); 23 | }); 24 | 25 | $client->on("message", function($message) use ($client, $logger){ 26 | $logger->notice("Got message: ".$message->getData()); 27 | $client->close(); 28 | }); 29 | 30 | $client->open()->then(function() use($logger, $client){ 31 | $logger->notice("We can use a promise to determine when the socket has been connected!"); 32 | }); 33 | 34 | $loop->run(); -------------------------------------------------------------------------------- /examples/headers.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Header Manipulation 5 | 6 | 7 | 8 | 9 |

PHPWS handshake response

10 |
11 | 
12 | 
13 | 14 | 20 | 21 | -------------------------------------------------------------------------------- /examples/headers.php: -------------------------------------------------------------------------------- 1 | #!/php -q 2 | addWriter($writer); 15 | 16 | // Create a WebSocket server using SSL 17 | $server = new WebSocketServer("tcp://0.0.0.0:12345", $loop, $logger); 18 | $server->on("handshake", function(WebSocketTransport $client, Handshake $handshake){ 19 | // Here we can alter or abort PHPWS's response to the user 20 | $handshake->getResponse()->getHeaders()->addHeaderLine("X-WebSocket-Server", "phpws"); 21 | 22 | // We can also see which headers the client sent in its handshake. Lets proof it 23 | $userAgent = $handshake->getRequest()->getHeader('User-Agent')->getFieldValue(); 24 | $handshake->getResponse()->getHeaders()->addHeaderLine("X-User-Agent",$userAgent); 25 | 26 | // Since we cannot see in the browser what headers were sent by the server, we will send them again as a message 27 | $client->on("connect", function() use ($client){ 28 | 29 | // The request and the response is available on the transport object as well. 30 | $client->sendString($client->getHandshakeResponse()->toString()); 31 | }); 32 | }); 33 | 34 | // Bind the server 35 | $server->bind(); 36 | 37 | // Start the event loop 38 | $loop->run(); -------------------------------------------------------------------------------- /examples/remoteEvents.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Protocol stacking 5 | 6 | 7 | 8 | 9 | 10 |

Remote Events

11 |

12 | 
13 | 
38 | 
39 | 


--------------------------------------------------------------------------------
/examples/remoteEvents.php:
--------------------------------------------------------------------------------
 1 |  php demo.php
 6 | use Devristo\Phpws\Messaging\RemoteEventMessage;
 7 | use Devristo\Phpws\Protocol\StackTransport;
 8 | use Devristo\Phpws\RemoteEvent\RemoteEventTransport;
 9 | use Devristo\Phpws\Protocol\ServerProtocolStack;
10 | use Devristo\Phpws\Protocol\TransportInterface;
11 | use Devristo\Phpws\Protocol\WebSocketTransportInterface;
12 | use Devristo\Phpws\Server\WebSocketServer;
13 | 
14 | 
15 | class StackHandler extends \Devristo\Phpws\Server\UriHandler\WebSocketUriHandler{
16 |     protected $loop;
17 | 
18 |     public function __construct(\React\EventLoop\LoopInterface $loop, $logger){
19 |         parent::__construct($logger);
20 |         $this->loop = $loop;
21 |     }
22 | 
23 |     /**
24 |      * Notify everyone when a user has joined the chat
25 |      *
26 |      * @param StackTransport $stackTransport
27 |      */
28 |     public function onConnect(WebSocketTransportInterface $transport){
29 |         /**
30 |          * @var $stackTransport StackTransport
31 |          * @var $jsonTransport RemoteEventTransport
32 |          */
33 |         $logger = $this->logger;
34 |         $loop = $this->loop;
35 |         $stackTransport = StackTransport::create($transport, array(function(TransportInterface $carrier) use($loop, $logger){
36 |             return new RemoteEventTransport($carrier, $loop, $logger);
37 |         }));
38 | 
39 |         $jsonTransport = $stackTransport->getTopTransport();
40 | 
41 |         $server = $transport->getHandshakeResponse()->getHeaders()->get('X-WebSocket-Server')->getFieldValue();
42 | 
43 |         $greetingMessage = RemoteEventMessage::create(null, "greeting", "hello world from $server!");
44 | 
45 |         $jsonTransport->whenResponseTo($greetingMessage, 0.1)->then(function(RemoteEventMessage $result) use ($logger, $server){
46 |             $logger->notice(sprintf("Got '%s' in response to 'hello world from $server!'", $result->getData()));
47 |         });
48 | 
49 |         $jsonTransport->remoteEvent()->on("greeting", function(RemoteEventMessage $message) use ($transport, $logger){
50 |             $logger->notice(sprintf("We got a greeting event from {$transport->getId()}"));
51 |         });
52 |     }
53 | }
54 | 
55 | $loop = \React\EventLoop\Factory::create();
56 | 
57 | // Create a logger which writes everything to the STDOUT
58 | $logger = new \Zend\Log\Logger();
59 | $writer = new \Zend\Log\Writer\Stream("php://output");
60 | $logger->addWriter($writer);
61 | 
62 | $server = new WebSocketServer("tcp://0.0.0.0:12345", $loop, $logger);
63 | $server->bind();
64 | 
65 | $server->on("handshake", function(\Devristo\Phpws\Protocol\WebSocketTransportInterface $transport, \Devristo\Phpws\Protocol\Handshake $handshake){
66 |     $handshake->getResponse()->getHeaders()->addHeaderLine("X-WebSocket-Server", "phpws");
67 | });
68 | 
69 | $router = new \Devristo\Phpws\Server\UriHandler\ClientRouter($server, $logger);
70 | $router->addRoute('#^/stack#i', new StackHandler($loop, $logger));
71 | 
72 | // Start the event loop
73 | $loop->run();


--------------------------------------------------------------------------------
/examples/rooms.html:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 | 
 4 |     Rooms
 5 |     
 6 |     
 7 | 
 8 | 
 9 | 
10 | 

Server Time

11 | 12 | 13 | 14 | 15 | 16 | 17 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /examples/rooms.php: -------------------------------------------------------------------------------- 1 | php demo.php 11 | use Devristo\Phpws\RemoteEvent\RemoteEventTransport; 12 | use Devristo\Phpws\Protocol\StackTransport; 13 | use Devristo\Phpws\Protocol\TransportInterface; 14 | use Devristo\Phpws\Server\WebSocketServer; 15 | 16 | $loop = \React\EventLoop\Factory::create(); 17 | 18 | // Create a logger which writes everything to the STDOUT 19 | $logger = new \Zend\Log\Logger(); 20 | $writer = new Zend\Log\Writer\Stream("php://output"); 21 | $logger->addWriter($writer); 22 | 23 | // Create a WebSocket server and create a router which sends all user requesting /echo to the DemoEchoHandler above 24 | $server = new WebSocketServer("tcp://0.0.0.0:12345", $loop, $logger); 25 | 26 | $handler = new \Devristo\Phpws\RemoteEvent\RemoteEvents($logger); 27 | 28 | $server->on("connect", function(TransportInterface $transport) use($loop, $logger, $handler){ 29 | 30 | $stack = StackTransport::create($transport, array( 31 | function (TransportInterface $transport) use ($loop, $logger) { 32 | return new RemoteEventTransport($transport, $loop, $logger); 33 | } 34 | )); 35 | 36 | $handler->listenTo($stack); 37 | }); 38 | 39 | $handler->room("time")->on("subscribe", function (StackTransport $transport) use ($logger, $handler){ 40 | $logger->notice("Someone joined our room full of time enthousiasts!!"); 41 | }); 42 | 43 | // Each 0.5 seconds sent the time to all connected clients 44 | $loop->addPeriodicTimer(0.5, function() use($server, $handler, $logger){ 45 | $time = new DateTime(); 46 | $string = $time->format("Y-m-d H:i:s"); 47 | 48 | if(count($handler->room("time")->getMembers())) 49 | $logger->notice("Broadcasting time to time room: $string"); 50 | 51 | $handler->room("time")->remoteEmit("time", $string); 52 | }); 53 | 54 | 55 | // Bind the server 56 | $server->bind(); 57 | 58 | // Start the event loop 59 | $loop->run(); -------------------------------------------------------------------------------- /examples/ssl_echo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | SSL Echo 4 | 5 | 10 | 11 | 71 | 72 | 73 | 74 |

WebSocket Test

75 |
76 | 77 | 78 | 79 |
Server will echo your response!
80 | 81 | -------------------------------------------------------------------------------- /examples/ssl_echo.php: -------------------------------------------------------------------------------- 1 | #!/php -q 2 | addWriter($writer); 15 | 16 | // Create a WebSocket server using SSL 17 | $server = new WebSocketServer("ssl://0.0.0.0:12345", $loop, $logger); 18 | $context = stream_context_create(); 19 | stream_context_set_option($context, 'ssl', 'local_cert', "democert.pem"); 20 | stream_context_set_option($context, 'ssl', 'allow_self_signed', true); 21 | stream_context_set_option($context, 'ssl', 'verify_peer', false); 22 | $server->setStreamContext($context); 23 | 24 | // Sent a welcome message when a client connects 25 | $server->on("connect", function(WebSocketTransportInterface $user){ 26 | $user->sendString("Hey! I am the echo robot. I will repeat all your input!"); 27 | }); 28 | 29 | // Echo back any message the user sends 30 | $server->on("message", function(WebSocketTransportInterface $user, WebSocketMessageInterface $message) use($logger){ 31 | $logger->notice(sprintf("We have got '%s' from client %s", $message->getData(), $user->getId())); 32 | $user->sendString($message->getData()); 33 | }); 34 | 35 | // Bind the server 36 | $server->bind(); 37 | 38 | // Start the event loop 39 | $loop->run(); -------------------------------------------------------------------------------- /examples/tcp_proxy_example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | TCP Proxy 5 | 6 | 7 | 8 | 9 |

Tcp proxy

10 |

Check JS Console for details

11 | 12 |

Request

13 |
 14 | 
 15 | 
16 | 17 |

Response

18 |
 19 | 
 20 | 
21 | 22 | 23 | 195 | 196 | 197 | -------------------------------------------------------------------------------- /examples/tcp_proxy_example.php: -------------------------------------------------------------------------------- 1 | #!/php -q 2 | php demo.php 7 | use Devristo\Phpws\Messaging\WebSocketMessageInterface; 8 | use Devristo\Phpws\Protocol\WebSocketTransportInterface; 9 | use Devristo\Phpws\Server\UriHandler\WebSocketUriHandler; 10 | use Devristo\Phpws\Server\WebSocketServer; 11 | 12 | /** 13 | * This URI handler will allow clients to open TCP connections through the outside world. Phpws is then acting as proxy. 14 | * 15 | * @author Chris 16 | * 17 | */ 18 | class ProxyHandler extends WebSocketUriHandler 19 | { 20 | /** 21 | * A multi-dimensional dictionary. First key is user id and second key is the id of the TCP stream. The value is the 22 | * TCP stream itself 23 | * 24 | * @var \React\Stream\Stream[][] 25 | */ 26 | protected $streams = array(); 27 | protected $server; 28 | 29 | /** 30 | * @param \React\EventLoop\LoopInterface $loop The React Loop, it is used to listen to events on newly created TCP 31 | * streams 32 | * @param $logger 33 | */ 34 | public function __construct(\React\EventLoop\LoopInterface $loop, $logger) 35 | { 36 | parent::__construct($logger); 37 | $this->loop = $loop; 38 | } 39 | 40 | public function onDisconnect(WebSocketTransportInterface $user) 41 | { 42 | foreach ($this->getStreamsByUser($user) as $stream) { 43 | $stream->close(); 44 | } 45 | unset($this->streams[$user->getId()]); 46 | } 47 | 48 | /** 49 | * Entry point for all messages received from clients in this proxy 'room' 50 | * 51 | * @param WebSocketTransportInterface $user 52 | * @param WebSocketMessageInterface $msg 53 | */ 54 | public function onMessage(WebSocketTransportInterface $user, WebSocketMessageInterface $msg) 55 | { 56 | try { 57 | $message = json_decode($msg->getData()); 58 | 59 | if ($message->command == 'connect') 60 | $this->requestConnect($user, $message); 61 | elseif ($message->command == 'write') 62 | $this->requestWrite($user, $message); 63 | elseif ($message->command == 'close') 64 | $this->requestClose($user, $message); 65 | 66 | } catch (Exception $e) { 67 | $this->logger->err($e->getMessage()); 68 | } 69 | } 70 | 71 | /** 72 | * Handler called when a CONNECT message is sent by a client 73 | * 74 | * A React SocketClient will be created, Google DNS is used to resolve host names. When the connection is made 75 | * several event listeners are attached. When data is received on the stream, it is forwarded to the client requesting 76 | * the proxied TCP connection 77 | * 78 | * Other events forwarded are connect and close 79 | * 80 | * @param WebSocketTransportInterface $user 81 | * @param $message 82 | */ 83 | protected function requestConnect(WebSocketTransportInterface $user, $message) 84 | { 85 | $address = $message->address; 86 | $this->logger->notice(sprintf("User %s requests connection to %s", $user->getId(), $address)); 87 | 88 | try { 89 | $dnsResolverFactory = new React\Dns\Resolver\Factory(); 90 | $dns = $dnsResolverFactory->createCached('8.8.8.8', $this->loop); 91 | $stream = new \React\SocketClient\Connector($this->loop, $dns); 92 | 93 | list($host, $port) = explode(":", $address); 94 | 95 | $logger = $this->logger; 96 | $that = $this; 97 | 98 | $stream->create($host, $port)->then(function (\React\Stream\Stream $stream) use($user, $logger, $message, $address, $that){ 99 | $id = uniqid("stream-$address-"); 100 | $that->addStream($user, $id, $stream); 101 | 102 | // Notify the user when the connection has been made 103 | $user->sendString(json_encode(array( 104 | 'connection' => $id, 105 | 'event' => 'connected', 106 | 'tag' => property_exists($message, 'tag') ? $message->tag : null 107 | ))); 108 | 109 | // Forward data back to the user 110 | $stream->on("data", function ($data) use ($stream, $id, $user, $logger){ 111 | $logger->notice("Forwarding ".strlen($data). " bytes from stream $id to {$user->getId()}"); 112 | $message = array( 113 | 'connection' => $id, 114 | 'event' => 'data', 115 | 'data' => $data 116 | ); 117 | 118 | $user->sendString(json_encode($message)); 119 | }); 120 | 121 | // When the stream closes, notify the user 122 | $stream->on("close", function() use($user, $id, $logger, $address){ 123 | $logger->notice(sprintf("Connection %s of user %s to %s has been closed", $id, $user->getId(), $address)); 124 | 125 | $message = 126 | array( 127 | 'connection' => $id, 128 | 'event' => 'close' 129 | ); 130 | 131 | $user->sendString(json_encode($message)); 132 | }); 133 | }); 134 | } catch (Exception $e) { 135 | $user->sendString(json_encode(array( 136 | 'event' => 'error', 137 | 'tag' => property_exists($message, 'tag') ? $message->tag : null, 138 | 'message' => $e->getMessage() 139 | ))); 140 | } 141 | } 142 | 143 | /** 144 | * Forward data send by the user over the specified TCP stream 145 | * 146 | * @param WebSocketTransportInterface $user 147 | * @param $message 148 | */ 149 | protected function requestWrite(WebSocketTransportInterface $user, $message) 150 | { 151 | $stream = $this->getStream($user, $message->connection); 152 | 153 | if($stream){ 154 | $this->logger->notice(sprintf("User %s writes %d bytes to connection %s", $user->getId(), strlen($message->data), $message->connection)); 155 | $stream->write($message->data); 156 | } 157 | } 158 | 159 | /** 160 | * Close the stream specified by the user 161 | * 162 | * @param WebSocketTransportInterface $user 163 | * @param $message 164 | */ 165 | protected function requestClose(WebSocketTransportInterface $user, $message) 166 | { 167 | $stream = $this->getStream($user, $message->connection); 168 | 169 | if($stream){ 170 | $this->logger->notice(sprintf("User %s closes connection %s", $user->getId(), $message->connection)); 171 | $stream->close(); 172 | $this->removeStream($user, $message->connection); 173 | 174 | $user->sendString(json_encode(array( 175 | 'event' => 'close', 176 | 'connection' => $message->connection, 177 | 'tag' => property_exists($message, 'tag') ? $message->tag : null 178 | ))); 179 | } else { 180 | $user->sendString(json_encode(array( 181 | 'event' => 'error', 182 | 'tag' => property_exists($message, 'tag') ? $message->tag : null, 183 | 'message' => 'Connection was already closed' 184 | ))); 185 | } 186 | } 187 | 188 | /** 189 | * @param WebSocketTransportInterface $user 190 | * @param $id 191 | * @return \React\Stream\Stream 192 | */ 193 | protected function getStream(WebSocketTransportInterface $user, $id) 194 | { 195 | $userStreams = $this->getStreamsByUser($user); 196 | 197 | return array_key_exists($id, $userStreams) ? $userStreams[$id] : null; 198 | } 199 | 200 | /** 201 | * @param WebSocketTransportInterface $user 202 | * @return \React\Stream\Stream[] 203 | */ 204 | protected function getStreamsByUser(WebSocketTransportInterface $user) 205 | { 206 | return array_key_exists($user->getId(), $this->streams) ? $this->streams[$user->getId()] : array(); 207 | } 208 | 209 | protected function removeStream(WebSocketTransportInterface $user, $id) 210 | { 211 | unset($this->streams[$user->getId()][$id]); 212 | } 213 | 214 | protected function addStream(WebSocketTransportInterface $user, $id, \React\Stream\Stream $stream){ 215 | $this->streams[$user->getId()][$id] = $stream; 216 | } 217 | } 218 | 219 | $loop = \React\EventLoop\Factory::create(); 220 | $logger = new \Zend\Log\Logger(); 221 | $writer = new Zend\Log\Writer\Stream("php://output"); 222 | $logger->addWriter($writer); 223 | 224 | $server = new WebSocketServer("tcp://0.0.0.0:12345", $loop, $logger); 225 | $router = new \Devristo\Phpws\Server\UriHandler\ClientRouter($server, $logger); 226 | $router->addRoute("#^/proxy$#i", new ProxyHandler($loop, $logger)); 227 | 228 | $server->bind(); 229 | $loop->run(); -------------------------------------------------------------------------------- /examples/time.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Timers 4 | 5 | 6 |

Server Time

7 |
Status:
8 |
Time:
9 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/time.php: -------------------------------------------------------------------------------- 1 | #!/php -q 2 | addWriter($writer); 12 | 13 | // Create a WebSocket server using SSL 14 | $server = new WebSocketServer("tcp://0.0.0.0:12345", $loop, $logger); 15 | 16 | // Each 0.5 seconds sent the time to all connected clients 17 | $loop->addPeriodicTimer(0.5, function() use($server, $logger){ 18 | $time = new DateTime(); 19 | $string = $time->format("Y-m-d H:i:s"); 20 | $logger->notice("Broadcasting time to all clients: $string"); 21 | foreach($server->getConnections() as $client) 22 | $client->sendString($string); 23 | }); 24 | 25 | 26 | // Bind the server 27 | $server->bind(); 28 | 29 | // Start the event loop 30 | $loop->run(); -------------------------------------------------------------------------------- /js/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phpws-js", 3 | "version": "2.0.0", 4 | "homepage": "https://github.com/Devristo/phpws", 5 | "authors": [ 6 | "Chris " 7 | ], 8 | "description": "PHPWS js libraries", 9 | "main": "js/phpws.js", 10 | "license": "MIT", 11 | "private": true, 12 | "dependencies": { 13 | "jquery": ">= 1.7.1" 14 | }, 15 | "ignore": [ 16 | "**/.*", 17 | "node_modules", 18 | "bower_components", 19 | "test", 20 | "tests" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /js/phpws.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Chris on 29-11-13. 3 | */ 4 | 5 | !function($){ 6 | function Emitter() { 7 | this.callbacks = {}; 8 | } 9 | 10 | Emitter.prototype.on = function(event, callback){ 11 | if(!(event in this.callbacks)) 12 | this.callbacks[event] = $.Callbacks(); 13 | 14 | this.callbacks[event].add(callback); 15 | return this; 16 | }; 17 | 18 | Emitter.prototype.removeListener = function(event, callback){ 19 | if(event in this.callbacks){ 20 | this.callbacks[event].remove.apply(this.callbacks[event], Array.prototype.slice.call(arguments, 1)); 21 | } 22 | 23 | return this; 24 | }; 25 | 26 | Emitter.prototype.once = function(event, callback){ 27 | var self = this; 28 | var _once = function(){ 29 | callback(); 30 | self.removeListener(event, arguments.callee); 31 | }; 32 | 33 | return this.on(event, _once); 34 | }; 35 | 36 | Emitter.prototype.trigger = function(event, data){ 37 | if(event in this.callbacks){ 38 | this.callbacks[event].fireWith(this, Array.prototype.slice.call(arguments, 1)); 39 | } 40 | 41 | return this; 42 | }; 43 | 44 | function Client(){ 45 | Emitter.apply(this, arguments); 46 | 47 | var self = this; 48 | var ws = null; 49 | var openPromise = $.Deferred(); 50 | var reconnectTimer = null; 51 | 52 | this.connect = function(url, autoReconnect){ 53 | try { 54 | ws = new WebSocket(url); 55 | }catch(error){ 56 | console.log("Cannot create WebSocket instance"); 57 | return openPromise; 58 | } 59 | 60 | ws.addEventListener('open', function () { 61 | openPromise.resolveWith(self); 62 | self.trigger("open", arguments); 63 | }); 64 | 65 | ws.addEventListener('close', function () { 66 | openPromise.resolveWith(self); 67 | self.trigger("close", arguments); 68 | }); 69 | 70 | ws.addEventListener('error', function () { 71 | openPromise.rejectWith(self); 72 | self.trigger("error", arguments); 73 | }); 74 | 75 | ws.addEventListener('message', function (event) { 76 | self.trigger("message", [event.data]); 77 | }); 78 | 79 | if (autoReconnect && !reconnectTimer) { 80 | reconnectTimer = setInterval( 81 | function () { 82 | if (ws.readyState != WebSocket.OPEN) 83 | self.connect(url); 84 | }, 85 | 5000 86 | ); 87 | } 88 | 89 | return openPromise; 90 | }; 91 | 92 | this.whenConnected = function(callback, context){ 93 | this.on("open", function(){callback.call(context);}); 94 | 95 | if(ws && ws.readyState == WebSocket.OPEN){ 96 | callback.call(context); 97 | } 98 | }; 99 | 100 | this.send = function(data){ 101 | return ws.send(data); 102 | }; 103 | 104 | this.close = function(){ 105 | return ws.close.apply(ws, arguments); 106 | }; 107 | }; 108 | 109 | Client.prototype = new Emitter; 110 | Client.prototype.constructor = Client; 111 | 112 | function Room (client, transport, name){ 113 | Emitter.apply(this, arguments); 114 | 115 | this.subscribed = true; 116 | this.name = name; 117 | this.transport = transport; 118 | this.client = client; 119 | } 120 | 121 | Room.prototype = new Emitter; 122 | Room.prototype.constructor = Room; 123 | 124 | Room.prototype.on = function(event, callback){ 125 | // Subscribe if we haven't done that yet 126 | if(!this.subscribed){ 127 | throw Error("Not subscribed to room " + this.name); 128 | } 129 | 130 | Emitter.prototype.on.apply(this, arguments); 131 | return this; 132 | }; 133 | 134 | Room.prototype.emit = function(event, data){ 135 | this.transport.emit(this.name, event, data); 136 | return this; 137 | }; 138 | 139 | Room.prototype.subscribe = function(){ 140 | var self = this; 141 | this.subscribed = true; 142 | this.client.whenConnected(function(){ 143 | console.log("Subscribing to room " + self.name); 144 | self.transport.emit(self.name, "subscribe"); 145 | }); 146 | return this; 147 | }; 148 | 149 | function EventTransport (client){ 150 | Emitter.apply(this, arguments); 151 | 152 | var self = this; 153 | var rooms = {}; 154 | var previousTag = 0; 155 | var generateTag = function(){ 156 | previousTag += 1; 157 | return "client-"+previousTag; 158 | }; 159 | 160 | var sendObj = function(obj){ 161 | client.send(JSON.stringify(obj)); 162 | }; 163 | 164 | var addReply = function(obj){ 165 | obj.reply = function(data){ 166 | var msg = { 167 | tag: obj.tag, 168 | data: obj.data, 169 | event: obj.event, 170 | room: obj.room 171 | }; 172 | 173 | sendObj(msg); 174 | }; 175 | }; 176 | 177 | client.on("message", function(message){ 178 | var messageObj = JSON.parse(message); 179 | addReply(messageObj); 180 | 181 | self.trigger(messageObj.event, messageObj); 182 | 183 | if('room' in messageObj){ 184 | self.room(messageObj.room).trigger(messageObj.event, messageObj); 185 | } 186 | }); 187 | 188 | this.room = function(name){ 189 | if(!(name in rooms)) 190 | rooms[name] = new Room(client, this, name); 191 | 192 | return rooms[name]; 193 | }; 194 | 195 | this.emit = function(room, event, args){ 196 | var msg = { 197 | room: room, 198 | tag: generateTag(), 199 | event: event, 200 | data: args 201 | }; 202 | 203 | sendObj(msg); 204 | 205 | return this; 206 | }; 207 | }; 208 | 209 | EventTransport.prototype = new Emitter; 210 | EventTransport.prototype.constructor = EventTransport; 211 | 212 | window.Phpws = { 213 | Client: Client, 214 | RemoteEvents: EventTransport 215 | }; 216 | 217 | }(window.jQuery); 218 | -------------------------------------------------------------------------------- /src/Devristo/Phpws/Client/Connector.php: -------------------------------------------------------------------------------- 1 | true, 30 | 'tls' => true, 31 | 'unix' => true, 32 | 33 | 'dns' => true, 34 | 'timeout' => true, 35 | ); 36 | 37 | if ($options['timeout'] === true) { 38 | $options['timeout'] = (float)ini_get("default_socket_timeout"); 39 | } 40 | 41 | if ($options['tcp'] instanceof ConnectorInterface) { 42 | $tcp = $options['tcp']; 43 | } else { 44 | $tcp = new TcpConnector( 45 | $loop, 46 | is_array($options['tcp']) ? $options['tcp'] : array() 47 | ); 48 | } 49 | 50 | if ($options['dns'] !== false) { 51 | if ($options['dns'] instanceof Resolver) { 52 | $resolver = $options['dns']; 53 | } else { 54 | $factory = new Factory(); 55 | $resolver = $factory->create( 56 | $options['dns'] === true ? '8.8.8.8' : $options['dns'], 57 | $loop 58 | ); 59 | } 60 | 61 | $tcp = new DnsConnector($tcp, $resolver); 62 | } 63 | 64 | if ($options['tcp'] !== false) { 65 | $options['tcp'] = $tcp; 66 | 67 | if ($options['timeout'] !== false) { 68 | $options['tcp'] = new TimeoutConnector( 69 | $options['tcp'], 70 | $options['timeout'], 71 | $loop 72 | ); 73 | } 74 | 75 | $this->connectors['tcp'] = $options['tcp']; 76 | } 77 | 78 | if ($options['tls'] !== false) { 79 | if (!$options['tls'] instanceof ConnectorInterface) { 80 | $options['tls'] = new SecureConnector( 81 | $tcp, 82 | $loop, 83 | is_array($options['tls']) ? $options['tls'] : array() 84 | ); 85 | } 86 | 87 | if ($options['timeout'] !== false) { 88 | $options['tls'] = new TimeoutConnector( 89 | $options['tls'], 90 | $options['timeout'], 91 | $loop 92 | ); 93 | } 94 | 95 | $this->connectors['tls'] = $options['tls']; 96 | } 97 | 98 | if ($options['unix'] !== false) { 99 | if (!$options['unix'] instanceof ConnectorInterface) { 100 | $options['unix'] = new UnixConnector($loop); 101 | } 102 | $this->connectors['unix'] = $options['unix']; 103 | } 104 | 105 | $options = null === $options ? array() : $options; 106 | $this->contextOptions = $options; 107 | } 108 | 109 | public function connect($uri) 110 | { 111 | $scheme = 'tcp'; 112 | if (strpos($uri, '://') !== false) { 113 | $scheme = (string)substr($uri, 0, strpos($uri, '://')); 114 | } 115 | 116 | if (!isset($this->connectors[$scheme])) { 117 | return Promise\Reject(new RuntimeException( 118 | 'No connector available for URI scheme "' . $scheme . '"' 119 | )); 120 | } 121 | 122 | return $this->connectors[$scheme]->connect($uri); 123 | } 124 | 125 | public function createSocketForAddress($address, $port, $hostName = null) 126 | { 127 | $url = $this->getSocketUrl($address, $port); 128 | 129 | $contextOpts = $this->contextOptions; 130 | // Fix for SSL in PHP >= 5.6, where peer name must be validated. 131 | if ($hostName !== null) { 132 | $contextOpts['ssl']['SNI_enabled'] = true; 133 | $contextOpts['ssl']['SNI_server_name'] = $hostName; 134 | $contextOpts['ssl']['peer_name'] = $hostName; 135 | } 136 | 137 | $flags = STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT; 138 | $context = stream_context_create($contextOpts); 139 | $socket = stream_socket_client($url, $errno, $errstr, 0, $flags, $context); 140 | 141 | if (!$socket) { 142 | return When::reject(new \RuntimeException( 143 | sprintf("connection to %s:%d failed: %s", $address, $port, $errstr), 144 | $errno 145 | )); 146 | } 147 | 148 | stream_set_blocking($socket, 0); 149 | 150 | // wait for connection 151 | 152 | return $this 153 | ->waitForStreamOnce($socket) 154 | ->then(array($this, 'checkConnectedSocket')) 155 | ->then(array($this, 'handleConnectedSocket')); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/Devristo/Phpws/Client/WebSocket.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 62 | $this->loop = $loop; 63 | $this->streamOptions = $streamOptions; 64 | $parts = parse_url($url); 65 | 66 | $this->url = $url; 67 | 68 | if (in_array($parts['scheme'], array('ws', 'wss')) === false) 69 | throw new WebSocketInvalidUrlScheme(); 70 | 71 | $dnsResolverFactory = new \React\Dns\Resolver\Factory(); 72 | $server = false === getenv('DNS_SERVER') ? '8.8.8.8' : getenv('DNS_SERVER'); 73 | $this->dns = $dnsResolverFactory->createCached($server, $loop); 74 | } 75 | 76 | public function open($timeOut=null) 77 | { 78 | /** 79 | * @var $that self 80 | */ 81 | $that = new FullAccessWrapper($this); 82 | 83 | $uri = new Uri($this->url); 84 | 85 | $isSecured = 'wss' === $uri->getScheme(); 86 | $defaultPort = $isSecured ? 443 : 80; 87 | 88 | $connector = new Connector($this->loop, $this->dns, $this->streamOptions); 89 | 90 | if ($isSecured) { 91 | $connector = new \React\Socket\SecureConnector($connector, $this->loop); 92 | } 93 | 94 | $deferred = new Deferred(); 95 | 96 | $port = $uri->getPort() ?: $defaultPort; 97 | $connector->connect($uri->getHost().':'.$port) 98 | ->then(function (ConnectionInterface $stream) use ($that, $uri, $deferred, $timeOut){ 99 | 100 | if($timeOut){ 101 | $timeOutTimer = $that->loop->addTimer($timeOut, function() use($deferred, $stream, $that){ 102 | $stream->close(); 103 | $that->logger->notice("Timeout occured, closing connection"); 104 | $that->emit("error"); 105 | $deferred->reject("Timeout occured"); 106 | }); 107 | } else $timeOutTimer = null; 108 | 109 | $transport = new WebSocketTransportHybi($stream); 110 | $transport->setLogger($that->logger); 111 | $that->transport = $transport; 112 | $that->stream = $stream; 113 | 114 | $stream->on("close", function() use($that){ 115 | $that->isClosing = false; 116 | $that->state = WebSocket::STATE_CLOSED; 117 | $that->emit('close'); 118 | }); 119 | 120 | // Give the chance to change request 121 | $transport->on("request", function(Request $handshake) use($that){ 122 | $that->emit("request", func_get_args()); 123 | }); 124 | 125 | $transport->on("handshake", function(Handshake $handshake) use($that){ 126 | $that->request = $handshake->getRequest(); 127 | $that->response = $handshake->getRequest(); 128 | 129 | $that->emit("handshake", array($handshake)); 130 | }); 131 | 132 | $transport->on("connect", function() use(&$state, $that, $transport, $timeOutTimer, $deferred){ 133 | if($timeOutTimer) 134 | $timeOutTimer->cancel(); 135 | 136 | $deferred->resolve($transport); 137 | $that->state = WebSocket::STATE_CONNECTED; 138 | $that->emit("connect"); 139 | 140 | }); 141 | 142 | $transport->on('message', function ($message) use ($that, $transport) { 143 | $that->emit("message", array($message)); 144 | }); 145 | 146 | $transport->initiateHandshake($uri); 147 | $that->state = WebSocket::STATE_HANDSHAKE_SENT; 148 | }, function($reason) use ($that, $deferred) 149 | { 150 | $deferred->reject($reason); 151 | $that->logger->err($reason); 152 | }); 153 | 154 | return $deferred->promise(); 155 | 156 | } 157 | 158 | public function send($string) 159 | { 160 | $this->transport->sendString($string); 161 | } 162 | 163 | public function sendMessage(WebSocketMessageInterface $msg) 164 | { 165 | $this->transport->sendMessage($msg); 166 | } 167 | 168 | public function sendFrame(WebSocketFrameInterface $frame) 169 | { 170 | $this->transport->sendFrame($frame); 171 | } 172 | 173 | public function close() 174 | { 175 | if ($this->isClosing) 176 | return; 177 | 178 | $this->isClosing = true; 179 | $this->sendFrame(WebSocketFrame::create(WebSocketOpcode::CloseFrame)); 180 | 181 | $this->state = self::STATE_CLOSING; 182 | $stream = $this->stream; 183 | 184 | $closeTimer = $this->loop->addTimer(5, function () use ($stream) { 185 | $stream->close(); 186 | }); 187 | 188 | $loop = $this->loop; 189 | $stream->once("close", function () use ($closeTimer, $loop) { 190 | if ($closeTimer) 191 | $loop->cancelTimer($closeTimer); 192 | }); 193 | } 194 | 195 | public function getState() 196 | { 197 | return $this->state; 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/Devristo/Phpws/Exceptions/WebSocketFrameSizeMismatch.php: -------------------------------------------------------------------------------- 1 | FIN = true; 44 | $o->payloadData = $data; 45 | $o->payloadLength = $data != null ? strlen($data) : 0; 46 | $o->setType($type); 47 | 48 | return $o; 49 | } 50 | 51 | public function setMasked($mask) 52 | { 53 | $this->mask = $mask ? 1 : 0; 54 | } 55 | 56 | public function isMasked() 57 | { 58 | return $this->mask == 1; 59 | } 60 | 61 | protected function setType($type) 62 | { 63 | $this->opcode = $type; 64 | 65 | if ($type == WebSocketOpcode::CloseFrame) 66 | $this->mask = 1; 67 | } 68 | 69 | protected static function IsBitSet($byte, $pos) 70 | { 71 | return ($byte & pow(2, $pos)) > 0 ? 1 : 0; 72 | } 73 | 74 | protected static function rotMask($data, $key, $offset = 0) 75 | { 76 | // Rotate key for example if $offset=1 and $key=abcd then output will be bcda 77 | $rotated_key = substr($key, $offset) . substr($key, 0, $offset); 78 | 79 | // Repeat key until it is at least the size of the $data 80 | $key_pad = str_repeat($rotated_key, ceil(1.0*strlen($data) / strlen($key))); 81 | 82 | return $data ^ substr($key_pad, 0, strlen($data)); 83 | } 84 | 85 | public function getType() 86 | { 87 | return $this->opcode; 88 | } 89 | 90 | public function encode() 91 | { 92 | $this->payloadLength = strlen($this->payloadData); 93 | 94 | $firstByte = $this->opcode; 95 | 96 | $firstByte += $this->FIN * 128 + $this->RSV1 * 64 + $this->RSV2 * 32 + $this->RSV3 * 16; 97 | 98 | $encoded = chr($firstByte); 99 | 100 | if ($this->payloadLength <= 125) { 101 | $secondByte = $this->payloadLength; 102 | $secondByte += $this->mask * 128; 103 | 104 | $encoded .= chr($secondByte); 105 | } else if ($this->payloadLength <= 256 * 256 - 1) { 106 | $secondByte = 126; 107 | $secondByte += $this->mask * 128; 108 | 109 | $encoded .= chr($secondByte) . pack("n", $this->payloadLength); 110 | } else { 111 | // TODO: max length is now 32 bits instead of 64 !!!!! 112 | $secondByte = 127; 113 | $secondByte += $this->mask * 128; 114 | 115 | $encoded .= chr($secondByte); 116 | $encoded .= pack("N", 0); 117 | $encoded .= pack("N", $this->payloadLength); 118 | } 119 | 120 | $key = 0; 121 | if ($this->mask) { 122 | $key = pack("N", rand(0, PHP_INT_MAX)); 123 | $encoded .= $key; 124 | } 125 | 126 | if ($this->payloadData) 127 | $encoded .= ($this->mask == 1) ? $this->rotMask($this->payloadData, $key) : $this->payloadData; 128 | 129 | return $encoded; 130 | } 131 | 132 | public static function decode(&$buffer) 133 | { 134 | if(strlen($buffer) < 2) 135 | return null; 136 | 137 | $frame = new self(); 138 | 139 | // Read the first two bytes, then chop them off 140 | $firstByte = substr($buffer, 0, 1); 141 | $secondByte = substr($buffer, 1, 1); 142 | $raw = substr($buffer, 2); 143 | 144 | $firstByte = ord($firstByte); 145 | $secondByte = ord($secondByte); 146 | 147 | $frame->FIN = self::IsBitSet($firstByte, 7); 148 | $frame->RSV1 = self::IsBitSet($firstByte, 6); 149 | $frame->RSV2 = self::IsBitSet($firstByte, 5); 150 | $frame->RSV3 = self::IsBitSet($firstByte, 4); 151 | 152 | $frame->mask = self::IsBitSet($secondByte, 7); 153 | 154 | $frame->opcode = ($firstByte & 0x0F); 155 | 156 | $len = $secondByte & ~128; 157 | 158 | if ($len <= 125){ 159 | $frame->payloadLength = $len; 160 | }elseif (($len == 126) && strlen($raw) >= 2){ 161 | $arr = unpack("nfirst", $raw); 162 | $frame->payloadLength = array_pop($arr); 163 | $raw = substr($raw, 2); 164 | } elseif (($len == 127) && strlen($raw) >= 8) { 165 | list(, $h, $l) = unpack('N2', $raw); 166 | $frame->payloadLength = ($l + ($h * 0x0100000000)); 167 | $raw = substr($raw, 8); 168 | } else{ 169 | return null; 170 | } 171 | 172 | // If the frame is masked, try to eat the key from the buffer. If the buffer is insufficient, return null and 173 | // try again next time 174 | if ($frame->mask) { 175 | if(strlen($raw) < 4) 176 | return null; 177 | 178 | $frame->maskingKey = substr($raw, 0, 4); 179 | $raw = substr($raw, 4); 180 | } 181 | 182 | 183 | // Don't continue until we have a full frame 184 | if(strlen($raw) < $frame->payloadLength) 185 | return null; 186 | 187 | $packetPayload = substr($raw, 0, $frame->payloadLength); 188 | 189 | // Advance buffer 190 | $buffer = substr($raw, $frame->payloadLength); 191 | 192 | if ($frame->mask) 193 | $frame->payloadData = self::rotMask($packetPayload, $frame->maskingKey, 0); 194 | else 195 | $frame->payloadData = $packetPayload; 196 | 197 | return $frame; 198 | } 199 | 200 | public function isReady() 201 | { 202 | if ($this->actualLength > $this->payloadLength) { 203 | throw new WebSocketFrameSizeMismatch($this); 204 | } 205 | return ($this->actualLength == $this->payloadLength); 206 | } 207 | 208 | public function isFinal() 209 | { 210 | return $this->FIN == 1; 211 | } 212 | 213 | public function getData() 214 | { 215 | return $this->payloadData; 216 | } 217 | 218 | } 219 | -------------------------------------------------------------------------------- /src/Devristo/Phpws/Framing/WebSocketFrame76.php: -------------------------------------------------------------------------------- 1 | payloadData = $data; 16 | 17 | return $o; 18 | } 19 | 20 | public function encode() 21 | { 22 | return chr(0) . $this->payloadData . chr(255); 23 | } 24 | 25 | public function getData() 26 | { 27 | return $this->payloadData; 28 | } 29 | 30 | public function getType() 31 | { 32 | return $this->opcode; 33 | } 34 | 35 | public static function decode(&$str) 36 | { 37 | $o = new self(); 38 | $o->payloadData = substr($str, 1, strlen($str) - 2); 39 | 40 | $str = ''; 41 | 42 | return $o; 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/Devristo/Phpws/Framing/WebSocketFrameInterface.php: -------------------------------------------------------------------------------- 1 | tag = uniqid("server-"); 20 | } 21 | 22 | public static function create($room, $event, $data) 23 | { 24 | $message = new RemoteEventMessage(); 25 | $message->setRoom($room); 26 | $message->setEvent($event); 27 | $message->setData($data); 28 | 29 | return $message; 30 | } 31 | 32 | /** 33 | * @param mixed $room 34 | */ 35 | public function setRoom($room) 36 | { 37 | $this->room = $room; 38 | } 39 | 40 | /** 41 | * @return mixed 42 | */ 43 | public function getRoom() 44 | { 45 | return $this->room; 46 | } 47 | 48 | /** 49 | * @param mixed $data 50 | */ 51 | public function setData($data) 52 | { 53 | $this->data = $data; 54 | } 55 | 56 | /** 57 | * @return mixed 58 | */ 59 | public function getData() 60 | { 61 | return $this->data; 62 | } 63 | 64 | /** 65 | * @param mixed $tag 66 | */ 67 | public function setTag($tag) 68 | { 69 | $this->tag = $tag; 70 | } 71 | 72 | /** 73 | * @return mixed 74 | */ 75 | public function getTag() 76 | { 77 | return $this->tag; 78 | } 79 | 80 | public static function fromJson($jsonString){ 81 | $data = json_decode($jsonString); 82 | 83 | if(!$data || !property_exists($data, 'event') || !property_exists($data, 'tag') || !property_exists($data, 'room')) 84 | throw new \InvalidArgumentException("Not a valid JSON RemoteEvent object"); 85 | 86 | $JsonMessage = new RemoteEventMessage(); 87 | 88 | if(property_exists($data, 'data')) 89 | $JsonMessage->setData($data->data); 90 | else $JsonMessage->setData(null); 91 | 92 | $JsonMessage->setTag($data->tag); 93 | $JsonMessage->setEvent($data->event); 94 | $JsonMessage->setRoom($data->room); 95 | 96 | return $JsonMessage; 97 | } 98 | 99 | public function toJson(){ 100 | return json_encode(array( 101 | 'tag' => $this->getTag(), 102 | 'data' => $this->getData(), 103 | 'room' => $this->getRoom(), 104 | 'event' => $this->getEvent() 105 | )); 106 | } 107 | 108 | /** 109 | * @param mixed $event 110 | */ 111 | public function setEvent($event) 112 | { 113 | $this->event = $event; 114 | } 115 | 116 | /** 117 | * @return mixed 118 | */ 119 | public function getEvent() 120 | { 121 | return $this->event; 122 | } 123 | } -------------------------------------------------------------------------------- /src/Devristo/Phpws/Messaging/WebSocketMessage.php: -------------------------------------------------------------------------------- 1 | data = $data; 30 | 31 | $this->createFrames(); 32 | } 33 | 34 | public static function create($data) 35 | { 36 | $o = new self(); 37 | 38 | $o->setData($data); 39 | return $o; 40 | } 41 | 42 | public function getData() 43 | { 44 | if ($this->isFinalised() == false) 45 | throw new WebSocketMessageNotFinalised($this); 46 | 47 | $data = ''; 48 | 49 | foreach ($this->frames as $frame) { 50 | $data .= $frame->getData(); 51 | } 52 | 53 | return $data; 54 | } 55 | 56 | public static function fromFrame(WebSocketFrameInterface $frame) 57 | { 58 | assert($frame instanceof WebSocketFrame); 59 | 60 | /** @var $frame \Devristo\Phpws\Framing\WebSocketFrame */ 61 | 62 | $o = new self(); 63 | $o->takeFrame($frame); 64 | 65 | return $o; 66 | } 67 | 68 | protected function createFrames() 69 | { 70 | $this->frames = array(WebSocketFrame::create(WebSocketOpcode::TextFrame, $this->data)); 71 | } 72 | 73 | public function getFrames() 74 | { 75 | return $this->frames; 76 | } 77 | 78 | public function isFinalised() 79 | { 80 | if (count($this->frames) == 0) 81 | return false; 82 | 83 | return $this->frames[count($this->frames) - 1]->isFinal(); 84 | } 85 | 86 | /** 87 | * Append a frame to the message 88 | * @param \Devristo\Phpws\Framing\WebSocketFrame $frame 89 | */ 90 | public function takeFrame(WebSocketFrame $frame) 91 | { 92 | $this->frames[] = $frame; 93 | } 94 | 95 | } -------------------------------------------------------------------------------- /src/Devristo/Phpws/Messaging/WebSocketMessage76.php: -------------------------------------------------------------------------------- 1 | setData($data); 36 | return $o; 37 | } 38 | 39 | public function getFrames() 40 | { 41 | $arr = array(); 42 | 43 | $arr[] = $this->frame; 44 | 45 | return $arr; 46 | } 47 | 48 | public function setData($data) 49 | { 50 | $this->data = $data; 51 | $this->frame = WebSocketFrame76::create(WebSocketOpcode::TextFrame, $data); 52 | } 53 | 54 | public function getData() 55 | { 56 | return $this->frame->getData(); 57 | } 58 | 59 | public function isFinalised() 60 | { 61 | return true; 62 | } 63 | 64 | /** 65 | * Creates a new WebSocketMessage76 from a IWebSocketFrame 66 | * @param WebSocketFrameInterface $frame 67 | * 68 | * @return \Devristo\Phpws\Messaging\WebSocketMessage76 Message composed of the frame provided 69 | */ 70 | public static function fromFrame(WebSocketFrameInterface $frame) 71 | { 72 | $o = new self(); 73 | $o->frame = $frame; 74 | 75 | return $o; 76 | } 77 | 78 | } -------------------------------------------------------------------------------- /src/Devristo/Phpws/Messaging/WebSocketMessageInterface.php: -------------------------------------------------------------------------------- 1 | request = $request; 22 | $this->response = $response; 23 | } 24 | 25 | public function getRequest(){ 26 | return $this->request; 27 | } 28 | 29 | public function getResponse(){ 30 | return $this->response; 31 | } 32 | 33 | public function abort(){ 34 | $this->abort = true; 35 | } 36 | 37 | public function isAborted(){ 38 | return $this->abort; 39 | } 40 | } -------------------------------------------------------------------------------- /src/Devristo/Phpws/Protocol/StackTransport.php: -------------------------------------------------------------------------------- 1 | TransportInterface 26 | $instantiator = function($spec, TransportInterface $carrier){ 27 | if(is_string($spec)){ 28 | $transport = new $spec($carrier); 29 | } elseif(is_callable($spec)){ 30 | $transport = $spec($carrier); 31 | } 32 | return $transport; 33 | }; 34 | 35 | $carrier = $webSocketTransport; 36 | $first = null; 37 | 38 | /** 39 | * @var $stack TransportInterface[] 40 | */ 41 | $stack = array($carrier); 42 | 43 | // Instantiate transports 44 | $i = 0; 45 | do{ 46 | $transport = $instantiator($stackSpecs[$i], new StackTransport($stack)); 47 | $stack[] = $transport; 48 | 49 | $i++; 50 | }while($i < count($stackSpecs)); 51 | 52 | $first = $stack[1]; 53 | $last = $stack[count($stack) - 1]; 54 | 55 | // Remember the stack for this websocket connection, used to trigger disconnect event 56 | return new StackTransport($stack); 57 | } 58 | 59 | public function __construct(array $stack){ 60 | if(count($stack) < 1) 61 | throw new \InvalidArgumentException("Stack must be a non-empty array"); 62 | 63 | $this->stack = $stack; 64 | } 65 | 66 | /** 67 | * @return WebSocketTransportInterface 68 | */ 69 | public function getWebSocketTransport(){ 70 | return $this->stack[0]; 71 | } 72 | 73 | /** 74 | * @return TransportInterface 75 | */ 76 | public function getTopTransport(){ 77 | return $this->stack[count($this->stack) - 1]; 78 | } 79 | 80 | /** 81 | * (PHP 5 >= 5.0.0)
82 | * Whether a offset exists 83 | * @link http://php.net/manual/en/arrayaccess.offsetexists.php 84 | * @param mixed $offset

85 | * An offset to check for. 86 | *

87 | * @return boolean true on success or false on failure. 88 | *

89 | *

90 | * The return value will be casted to boolean if non-boolean was returned. 91 | */ 92 | public function offsetExists($offset) 93 | { 94 | return $offset < count($this->stack); 95 | } 96 | 97 | /** 98 | * (PHP 5 >= 5.0.0)
99 | * Offset to retrieve 100 | * @link http://php.net/manual/en/arrayaccess.offsetget.php 101 | * @param mixed $offset

102 | * The offset to retrieve. 103 | *

104 | * @return mixed Can return all value types. 105 | */ 106 | public function offsetGet($offset) 107 | { 108 | return $this->stack[$offset]; 109 | } 110 | 111 | /** 112 | * (PHP 5 >= 5.0.0)
113 | * Offset to set 114 | * @link http://php.net/manual/en/arrayaccess.offsetset.php 115 | * @param mixed $offset

116 | * The offset to assign the value to. 117 | *

118 | * @param mixed $value

119 | * The value to set. 120 | *

121 | * @return void 122 | */ 123 | public function offsetSet($offset, $value) 124 | { 125 | throw new \BadMethodCallException("Immutable stack, cannot set element"); 126 | } 127 | 128 | /** 129 | * (PHP 5 >= 5.0.0)
130 | * Offset to unset 131 | * @link http://php.net/manual/en/arrayaccess.offsetunset.php 132 | * @param mixed $offset

133 | * The offset to unset. 134 | *

135 | * @return void 136 | */ 137 | public function offsetUnset($offset) 138 | { 139 | throw new \BadMethodCallException("Immutable stack, cannot set element"); 140 | } 141 | 142 | public function on($event, callable $listener) 143 | { 144 | return $this->getTopTransport()->on($event, $listener); 145 | } 146 | 147 | public function once($event, callable $listener) 148 | { 149 | return $this->getTopTransport()->once($event, $listener); 150 | } 151 | 152 | public function removeListener($event, callable $listener) 153 | { 154 | return $this->getTopTransport()->removeListener($event, $listener); 155 | } 156 | 157 | public function removeAllListeners($event = null) 158 | { 159 | return $this->getTopTransport()->removeAllListeners($event); 160 | } 161 | 162 | public function listeners($event) 163 | { 164 | return $this->getTopTransport()->listeners($event); 165 | } 166 | 167 | public function emit($event, array $arguments = array()) 168 | { 169 | return $this->getTopTransport()->emit($event, $arguments); 170 | } 171 | 172 | public function getId() 173 | { 174 | return $this->getWebSocketTransport()->getId(); 175 | } 176 | 177 | public function respondTo(Request $request) 178 | { 179 | throw new \BadMethodCallException(); 180 | } 181 | 182 | public function handleData(&$data) 183 | { 184 | throw new \BadMethodCallException(); 185 | } 186 | 187 | public function sendString($msg) 188 | { 189 | $this->getTopTransport()->sendString($msg); 190 | } 191 | 192 | public function getIp() 193 | { 194 | $this->getWebSocketTransport()->getIp(); 195 | } 196 | 197 | public function close() 198 | { 199 | $this->getWebSocketTransport()->close(); 200 | } 201 | 202 | /** 203 | * @return Request 204 | */ 205 | public function getHandshakeRequest() 206 | { 207 | return $this->getWebSocketTransport()->getHandshakeRequest(); 208 | } 209 | 210 | /** 211 | * @return Response 212 | */ 213 | public function getHandshakeResponse() 214 | { 215 | return $this->getWebSocketTransport()->getHandshakeResponse(); 216 | } 217 | 218 | public function setData($key, $value){ 219 | $this->getWebSocketTransport()->setData($key, $value); 220 | } 221 | 222 | public function getData($key){ 223 | return $this->getWebSocketTransport()->getData($key); 224 | } 225 | } -------------------------------------------------------------------------------- /src/Devristo/Phpws/Protocol/TransportInterface.php: -------------------------------------------------------------------------------- 1 | _lastChanged = time(); 30 | $this->logger = $logger; 31 | } 32 | 33 | public function handleData($stream) 34 | { 35 | if (feof($stream) || !is_resource($stream)){ 36 | $this->close(); 37 | return; 38 | } 39 | 40 | $data = fread($stream, $this->bufferSize); 41 | if ('' === $data || false === $data) { 42 | $this->close(); 43 | } else { 44 | $this->onData($data); 45 | } 46 | } 47 | 48 | private function onData($data) 49 | { 50 | try { 51 | $this->_lastChanged = time(); 52 | 53 | if ($this->_transport) 54 | $this->emit('data', array($data, $this)); 55 | else 56 | $this->establishConnection($data); 57 | } catch (Exception $e) { 58 | $this->logger->err("Error while handling incoming data. Exception message is: ".$e->getMessage()); 59 | $this->close(); 60 | } 61 | } 62 | 63 | public function setTransport(WebSocketTransportInterface $con) 64 | { 65 | $this->_transport = $con; 66 | } 67 | 68 | public function establishConnection($data) 69 | { 70 | $this->_transport = WebSocketTransportFactory::fromSocketData($this, $data, $this->logger); 71 | $myself = $this; 72 | 73 | $this->_transport->on("handshake", function(Handshake $request) use ($myself){ 74 | $myself->emit("handshake", array($request)); 75 | }); 76 | 77 | $this->_transport->on("connect", function() use ($myself){ 78 | $myself->emit("connect", array($myself)); 79 | }); 80 | 81 | $this->_transport->on("message", function($message) use($myself){ 82 | $myself->emit("message", array("message" => $message)); 83 | }); 84 | 85 | $this->_transport->on("flashXmlRequest", function($message) use($myself){ 86 | $myself->emit("flashXmlRequest"); 87 | }); 88 | 89 | if ($this->_transport instanceof WebSocketTransportFlash) 90 | return; 91 | 92 | $request = Request::fromString($data); 93 | $this->_transport->respondTo($request); 94 | } 95 | 96 | public function getLastChanged() 97 | { 98 | return $this->_lastChanged; 99 | } 100 | 101 | /** 102 | * 103 | * @return WebSocketTransportInterface 104 | */ 105 | public function getTransport() 106 | { 107 | return $this->_transport; 108 | } 109 | 110 | public function setLogger(LoggerInterface $logger) 111 | { 112 | $this->logger = $logger; 113 | } 114 | } -------------------------------------------------------------------------------- /src/Devristo/Phpws/Protocol/WebSocketTransport.php: -------------------------------------------------------------------------------- 1 | _socket = $socket; 53 | $this->_id = uniqid("connection-"); 54 | 55 | $that = $this; 56 | 57 | $buffer = ''; 58 | 59 | $socket->on("data", function($data) use ($that, &$buffer){ 60 | $buffer .= $data; 61 | $that->handleData($buffer); 62 | }); 63 | 64 | $socket->on("close", function($data = null) use ($that){ 65 | $that->emit("close", func_get_args()); 66 | }); 67 | } 68 | 69 | public function getIp() 70 | { 71 | return $this->_socket->getRemoteAddress(); 72 | } 73 | 74 | public function getId() 75 | { 76 | return $this->_id; 77 | } 78 | 79 | protected function setRequest(Request $request){ 80 | $this->request = $request; 81 | } 82 | 83 | protected function setResponse(Response $response){ 84 | $this->response = $response; 85 | } 86 | 87 | public function getHandshakeRequest(){ 88 | return $this->request; 89 | } 90 | 91 | public function getHandshakeResponse(){ 92 | return $this->response; 93 | } 94 | 95 | public function getSocket() 96 | { 97 | return $this->_socket; 98 | } 99 | 100 | public function setLogger(LoggerInterface $logger){ 101 | $this->logger = $logger; 102 | } 103 | 104 | public function sendFrame(WebSocketFrameInterface $frame) 105 | { 106 | if ($this->_socket->write($frame->encode()) === false) 107 | return false; 108 | 109 | return true; 110 | } 111 | 112 | public function sendMessage(WebSocketMessageInterface $msg) 113 | { 114 | foreach ($msg->getFrames() as $frame) { 115 | if ($this->sendFrame($frame) === false) 116 | return false; 117 | } 118 | 119 | return true; 120 | } 121 | 122 | public function setData($key, $value){ 123 | $this->data[$key] = $value; 124 | } 125 | 126 | public function getData($key){ 127 | return $this->data[$key]; 128 | } 129 | } -------------------------------------------------------------------------------- /src/Devristo/Phpws/Protocol/WebSocketTransportFactory.php: -------------------------------------------------------------------------------- 1 | ') === 0){ 22 | $s = new WebSocketTransportFlash($socket, $data); 23 | $s->setLogger($logger); 24 | 25 | return $s; 26 | } 27 | 28 | $request = Request::fromString($data); 29 | 30 | if ($request->getHeader('Sec-Websocket-Key1')) { 31 | $s = new WebSocketTransportHixie($socket, $request, $data); 32 | $s->setLogger($logger); 33 | } else{ 34 | $s = new WebSocketTransportHybi($socket, $request); 35 | $s->setLogger($logger); 36 | } 37 | 38 | 39 | return $s; 40 | } 41 | } -------------------------------------------------------------------------------- /src/Devristo/Phpws/Protocol/WebSocketTransportFlash.php: -------------------------------------------------------------------------------- 1 | _socket = $socket; 19 | 20 | $this->emit("flashXmlRequest"); 21 | } 22 | 23 | public function sendString($msg) 24 | { 25 | $this->_socket->write($msg); 26 | } 27 | 28 | public function close() 29 | { 30 | $this->_socket->close(); 31 | } 32 | 33 | public function sendHandshakeResponse() 34 | { 35 | throw new Exception("Not supported!"); 36 | } 37 | 38 | public function handleData($data) 39 | { 40 | throw new Exception("Not supported!"); 41 | } 42 | } -------------------------------------------------------------------------------- /src/Devristo/Phpws/Protocol/WebSocketTransportHixie.php: -------------------------------------------------------------------------------- 1 | request = $request; 18 | $this->sendHandshakeResponse(); 19 | } 20 | 21 | private function sendHandshakeResponse() 22 | { 23 | // Last 8 bytes of the client's handshake are used for key calculation later 24 | $l8b = $this->request->getContent(); 25 | 26 | // Check for 2-key based handshake (Hixie protocol draft) 27 | $key1 = $this->getHandshakeRequest()->getHeader('Sec-Websocket-Key1')->getFieldValue(); 28 | $key2 = $this->getHandshakeRequest()->getHeader('Sec-Websocket-Key2')->getFieldValue(); 29 | 30 | // Origin checking (TODO) 31 | $originHeader = $this->getHandshakeRequest()->getHeader('Origin', null); 32 | $host = $this->getHandshakeRequest()->getHeader('Host')->getFieldValue(); 33 | $location = $this->getHandshakeRequest()->getUriString(); 34 | 35 | // Build response 36 | $response = new Response(); 37 | $response->setStatusCode(101); 38 | $response->setReasonPhrase("WebSocket Protocol Handshake"); 39 | 40 | $headers = new Headers(); 41 | $response->setHeaders($headers); 42 | 43 | $headers->addHeaderLine("Upgrade", "WebSocket"); 44 | $headers->addHeaderLine("Connection", "Upgrade"); 45 | 46 | if($originHeader) 47 | $headers->addHeaderLine("Sec-WebSocket-Origin", $originHeader->getFieldValue()); 48 | $headers->addHeaderLine("Sec-WebSocket-Location", "ws://{$host}$location"); 49 | 50 | // Build HIXIE response 51 | $response->setContent(self::calcHixieResponse($key1, $key2, $l8b)); 52 | 53 | $this->setResponse($response); 54 | 55 | $handshakeRequest = new Handshake($this->getHandshakeRequest(), $this->getHandshakeResponse()); 56 | $this->emit("handshake", array($handshakeRequest)); 57 | 58 | if($handshakeRequest->isAborted()) 59 | $this->close(); 60 | else { 61 | $this->_socket->write($response->toString()); 62 | $this->logger->debug("Got an HYBI style request, sent HYBY handshake response"); 63 | 64 | $this->emit("connect"); 65 | } 66 | } 67 | 68 | /** 69 | * Calculate the #76 draft key based on the 2 challenges from the client and the last 8 bytes of the request 70 | * 71 | * @param string $key1 Sec-WebSocket-Key1 72 | * @param string $key2 Sec-Websocket-Key2 73 | * @param string $l8b Last 8 bytes of the client's opening handshake 74 | * 75 | * @throws \Devristo\Phpws\Exceptions\WebSocketInvalidKeyException 76 | * @return string Hixie compatible response to client's challenge 77 | */ 78 | private static function calcHixieResponse($key1, $key2, $l8b) 79 | { 80 | // Get the numbers from the opening handshake 81 | $numbers1 = preg_replace("/[^0-9]/", "", $key1); 82 | $numbers2 = preg_replace("/[^0-9]/", "", $key2); 83 | 84 | //Count spaces 85 | $spaces1 = substr_count($key1, " "); 86 | $spaces2 = substr_count($key2, " "); 87 | 88 | if ($spaces1 == 0 || $spaces2 == 0) { 89 | throw new WebSocketInvalidKeyException($key1, $key2, $l8b); 90 | } 91 | 92 | // Key is the number divided by the amount of spaces expressed as a big-endian 32 bit integer 93 | $key1_sec = pack("N", $numbers1 / $spaces1); 94 | $key2_sec = pack("N", $numbers2 / $spaces2); 95 | 96 | // The response is the md5-hash of the 2 keys and the last 8 bytes of the opening handshake, expressed as a binary string 97 | return md5($key1_sec . $key2_sec . $l8b, 1); 98 | } 99 | 100 | 101 | public function handleData(&$data) 102 | { 103 | $f = WebSocketFrame76::decode($data); 104 | $message = WebSocketMessage76::fromFrame($f); 105 | 106 | $this->emit("message", array('message' => $message)); 107 | 108 | return array($f); 109 | } 110 | 111 | public function sendString($msg) 112 | { 113 | $m = WebSocketMessage76::create($msg); 114 | 115 | return $this->sendMessage($m); 116 | } 117 | 118 | public function close() 119 | { 120 | $this->_socket->close(); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/Devristo/Phpws/Protocol/WebSocketTransportHybi.php: -------------------------------------------------------------------------------- 1 | request = $request; 38 | $this->_role = WebsocketTransportRole::SERVER; 39 | $this->sendHandshakeResponse(); 40 | } 41 | 42 | private function sendHandshakeResponse() 43 | { 44 | try{ 45 | $challengeHeader = $this->getHandshakeRequest()->getHeader('Sec-Websocket-Key', null); 46 | 47 | if(!$challengeHeader) 48 | throw new Exception("No Sec-WebSocket-Key received!"); 49 | 50 | // Check for newer handshake 51 | $challenge = $challengeHeader->getFieldValue(); 52 | 53 | // Build response 54 | $response = new Response(); 55 | $response->setStatusCode(101); 56 | $response->setReasonPhrase("WebSocket Protocol Handshake"); 57 | 58 | $headers = new Headers(); 59 | $response->setHeaders($headers); 60 | 61 | $headers->addHeaderLine("Upgrade", "WebSocket"); 62 | $headers->addHeaderLine("Connection", "Upgrade"); 63 | $headers->addHeaderLine("Sec-WebSocket-Accept", self::calcHybiResponse($challenge)); 64 | 65 | $this->setResponse($response); 66 | 67 | $handshakeRequest = new Handshake($this->getHandshakeRequest(), $this->getHandshakeResponse()); 68 | $this->emit("handshake", array($handshakeRequest)); 69 | 70 | if($handshakeRequest->isAborted()) 71 | $this->close(); 72 | else { 73 | $this->_socket->write($response->toString()); 74 | $this->logger->debug("Got an HYBI style request, sent HYBY handshake response"); 75 | 76 | $this->connected = true; 77 | $this->emit("connect"); 78 | } 79 | } catch(Exception $e){ 80 | $this->logger->err("Connection error, message: ".$e->getMessage()); 81 | $this->close(); 82 | } 83 | } 84 | 85 | private static function calcHybiResponse($challenge) 86 | { 87 | return base64_encode(sha1($challenge . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true)); 88 | } 89 | 90 | private static function containsCompleteHeader($data) { 91 | return strstr($data, "\r\n\r\n"); 92 | } 93 | 94 | public function handleData(&$data) 95 | { 96 | if(!$this->connected) 97 | { 98 | if (!$this->containsCompleteHeader($data)) { 99 | return array(); 100 | } 101 | $data = $this->readHandshakeResponse($data); 102 | } 103 | 104 | $frames = array(); 105 | while ($frame = WebSocketFrame::decode($data)){ 106 | if (WebSocketOpcode::isControlFrame($frame->getType())) 107 | $this->processControlFrame($frame); 108 | else 109 | $this->processMessageFrame($frame); 110 | 111 | $frames[] = $frame; 112 | } 113 | 114 | return $frames; 115 | } 116 | 117 | public function sendFrame(WebSocketFrameInterface $frame) 118 | { 119 | /** 120 | * @var $hybiFrame WebSocketFrame 121 | */ 122 | $hybiFrame = $frame; 123 | 124 | // Mask IFF client! 125 | $hybiFrame->setMasked($this->_role == WebsocketTransportRole::CLIENT); 126 | 127 | parent::sendFrame($hybiFrame); 128 | } 129 | 130 | /** 131 | * Process a Message Frame 132 | * 133 | * Appends or creates a new message and attaches it to the user sending it. 134 | * 135 | * When the last frame of a message is received, the message is sent for processing to the 136 | * abstract WebSocket::onMessage() method. 137 | * 138 | * @param WebSocketFrame $frame 139 | */ 140 | protected function processMessageFrame(WebSocketFrame $frame) 141 | { 142 | if ($this->_openMessage && $this->_openMessage->isFinalised() == false) { 143 | $this->_openMessage->takeFrame($frame); 144 | } else { 145 | $this->_openMessage = WebSocketMessage::fromFrame($frame); 146 | } 147 | 148 | if ($this->_openMessage && $this->_openMessage->isFinalised()) { 149 | $this->emit("message", array($this->_openMessage)); 150 | $this->_openMessage = null; 151 | } 152 | } 153 | 154 | /** 155 | * Handle incoming control frames 156 | * 157 | * Sends Pong on Ping and closes the connection after a Close request. 158 | * 159 | * @param WebSocketFrame $frame 160 | */ 161 | protected function processControlFrame(WebSocketFrame $frame) 162 | { 163 | switch ($frame->getType()) { 164 | case WebSocketOpcode::CloseFrame : 165 | $this->logger->notice("Got CLOSE frame"); 166 | 167 | $frame = WebSocketFrame::create(WebSocketOpcode::CloseFrame); 168 | $this->sendFrame($frame); 169 | 170 | $this->_socket->close(); 171 | break; 172 | case WebSocketOpcode::PingFrame : 173 | $frame = WebSocketFrame::create(WebSocketOpcode::PongFrame); 174 | $this->sendFrame($frame); 175 | break; 176 | } 177 | } 178 | 179 | public function sendString($msg) 180 | { 181 | try { 182 | $m = WebSocketMessage::create($msg); 183 | 184 | return $this->sendMessage($m); 185 | } catch (Exception $e) { 186 | $this->close(); 187 | } 188 | 189 | return false; 190 | } 191 | 192 | public function close() 193 | { 194 | $f = WebSocketFrame::create(WebSocketOpcode::CloseFrame); 195 | $this->sendFrame($f); 196 | 197 | $this->_socket->close(); 198 | } 199 | 200 | private static function randHybiKey() 201 | { 202 | return base64_encode( 203 | chr(rand(0, 255)) . chr(rand(0, 255)) . chr(rand(0, 255)) . chr(rand(0, 255)) 204 | . chr(rand(0, 255)) . chr(rand(0, 255)) . chr(rand(0, 255)) . chr(rand(0, 255)) 205 | . chr(rand(0, 255)) . chr(rand(0, 255)) . chr(rand(0, 255)) . chr(rand(0, 255)) 206 | . chr(rand(0, 255)) . chr(rand(0, 255)) . chr(rand(0, 255)) . chr(rand(0, 255)) 207 | ); 208 | } 209 | 210 | public function initiateHandshake(Uri $uri) 211 | { 212 | $challenge = self::randHybiKey(); 213 | 214 | $request = new Request(); 215 | 216 | $requestUri = $uri->getPath(); 217 | 218 | if($uri->getQuery()) 219 | $requestUri .= "?".$uri->getQuery(); 220 | 221 | 222 | $request->setUri($requestUri); 223 | 224 | $request->getHeaders()->addHeaderLine("Connection", "Upgrade"); 225 | $request->getHeaders()->addHeaderLine("Host", $uri->getHost()); 226 | $request->getHeaders()->addHeaderLine("Sec-WebSocket-Key", $challenge); 227 | $request->getHeaders()->addHeaderLine("Sec-WebSocket-Version", 13); 228 | $request->getHeaders()->addHeaderLine("Upgrade", "websocket"); 229 | 230 | $this->setRequest($request); 231 | 232 | $this->emit("request", array($request)); 233 | 234 | $this->_socket->write($request->toString()); 235 | 236 | return $request; 237 | } 238 | 239 | private function readHandshakeResponse($data) 240 | { 241 | $response = Response::fromString($data); 242 | $this->setResponse($response); 243 | 244 | $handshake = new Handshake($this->request, $response); 245 | 246 | $this->emit("handshake", array($handshake)); 247 | 248 | if($handshake->isAborted()){ 249 | $this->close(); 250 | return ''; 251 | } 252 | 253 | $this->connected = true; 254 | $this->emit("connect"); 255 | 256 | return $response->getContent(); 257 | } 258 | 259 | } 260 | -------------------------------------------------------------------------------- /src/Devristo/Phpws/Protocol/WebSocketTransportInterface.php: -------------------------------------------------------------------------------- 1 | _self = $self; 18 | $this->_refl = new \ReflectionObject($self); 19 | } 20 | 21 | public function __call($method, $args) 22 | { 23 | $mrefl = $this->_refl->getMethod($method); 24 | $mrefl->setAccessible(true); 25 | return $mrefl->invokeArgs($this->_self, $args); 26 | } 27 | 28 | public function __set($name, $value) 29 | { 30 | $prefl = $this->_refl->getProperty($name); 31 | $prefl->setAccessible(true); 32 | $prefl->setValue($this->_self, $value); 33 | } 34 | 35 | public function __get($name) 36 | { 37 | $prefl = $this->_refl->getProperty($name); 38 | $prefl->setAccessible(true); 39 | return $prefl->getValue($this->_self); 40 | } 41 | 42 | public function __isset($name) 43 | { 44 | $value = $this->__get($name); 45 | return isset($value); 46 | } 47 | } -------------------------------------------------------------------------------- /src/Devristo/Phpws/RemoteEvent/RemoteEventTransport.php: -------------------------------------------------------------------------------- 1 | actionEmitter; 36 | } 37 | 38 | public function __construct(TransportInterface $carrierProtocol, LoopInterface $loop, LoggerInterface $logger){ 39 | $that = $this; 40 | $this->logger = $logger; 41 | $this->loop = $loop; 42 | $this->carrierProtocol = $carrierProtocol; 43 | 44 | $this->actionEmitter = new EventEmitter(); 45 | 46 | $deferreds = &$this->deferred; 47 | $timers = &$this->timers; 48 | 49 | $carrierProtocol->on("message", function(MessageInterface $message) use (&$deferreds, &$timers, &$loop, $that, $logger){ 50 | $string = $message->getData(); 51 | 52 | try{ 53 | $jsonMessage = RemoteEventMessage::fromJson($string); 54 | 55 | $tag = $jsonMessage->getTag(); 56 | 57 | if(array_key_exists($tag, $deferreds)){ 58 | $deferred = $deferreds[$tag]; 59 | unset($deferreds[$tag]); 60 | 61 | if(array_key_exists($tag, $timers)){ 62 | $loop->cancelTimer($timers[$tag]); 63 | unset($timers[$tag]); 64 | } 65 | $deferred->resolve($jsonMessage); 66 | }else 67 | $that->remoteEvent()->emit($jsonMessage->getEvent(), array($jsonMessage)); 68 | $that->emit("message", array($jsonMessage)); 69 | 70 | }catch(\Exception $e){ 71 | $logger->err("Exception while parsing JsonMessage: ".$e->getMessage()); 72 | } 73 | }); 74 | } 75 | 76 | public function replyTo(RemoteEventMessage $message, $data){ 77 | $reply = new RemoteEventMessage(); 78 | $reply->setRoom($message->getRoom()); 79 | $reply->setTag($message->getTag()); 80 | $reply->setEvent($message->getEvent()); 81 | $reply->setData($data); 82 | 83 | $this->carrierProtocol->sendString($reply->toJson()); 84 | } 85 | 86 | public function whenResponseTo(RemoteEventMessage $message, $timeout=null){ 87 | $deferred = new Deferred(); 88 | 89 | $tag = $message->getTag(); 90 | $this->deferred[$tag] = $deferred; 91 | 92 | $this->carrierProtocol->sendString($message->toJson()); 93 | $this->logger->debug(sprintf( 94 | "Awaiting response to '%s'%s with %s" 95 | , $message->getData() 96 | , $message->getRoom() ? " in room ".$message->getRoom() : '' 97 | , $timeout ? "timeout $timeout" : 'no timeout' 98 | )); 99 | 100 | if($timeout){ 101 | $list = &$this->deferred; 102 | $logger = $this->logger; 103 | 104 | $this->timers[$tag] = $this->loop->addTimer($timeout, function() use($deferred, &$list, $tag, $logger){ 105 | unset($list[$tag]); 106 | $logger->debug("Request with tag $tag has timed out"); 107 | $deferred->reject("Timeout occurred"); 108 | }); 109 | } 110 | 111 | return $deferred->promise(); 112 | } 113 | 114 | public function sendEmit($room, $event, $data){ 115 | $message = RemoteEventMessage::create($room, $event, $data); 116 | $this->send($message); 117 | } 118 | 119 | public function sendString($string) 120 | { 121 | $message = new RemoteEventMessage(); 122 | $message->setTag(uniqid("server-")); 123 | $message->setData($string); 124 | 125 | $this->send($message); 126 | } 127 | 128 | public function send(RemoteEventMessage $message) 129 | { 130 | $this->carrierProtocol->sendString($message->toJson()); 131 | } 132 | } -------------------------------------------------------------------------------- /src/Devristo/Phpws/RemoteEvent/RemoteEvents.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 20 | } 21 | 22 | /** 23 | * @param $room 24 | * @return Room 25 | */ 26 | public function room($room) 27 | { 28 | if (!array_key_exists($room, $this->rooms)) 29 | $this->rooms[$room] = new Room($room, $this->logger); 30 | 31 | return $this->rooms[$room]; 32 | } 33 | 34 | public function listenTo(StackTransport $transport) 35 | { 36 | $self = $this; 37 | $transport->on("message", function (RemoteEventMessage $message) use ($transport, $self) { 38 | $room = $message->getRoom(); 39 | 40 | if (!$room) 41 | return; 42 | 43 | $event = $message->getEvent(); 44 | 45 | if ($message->getEvent() == 'subscribe'){ 46 | $self->room($room)->subscribe($transport); 47 | 48 | // If the transport is disconnected, make sure we 'fake' the unsubscribe 49 | $transport->getWebSocketTransport()->on("close", function() use ($self, $transport, $room){ 50 | $self->room($room)->unsubscribe($transport); 51 | 52 | // Fake unsubscribe message 53 | $message = new RemoteEventMessage(); 54 | $message->setEvent("unsubscribe"); 55 | $message->setRoom($room); 56 | 57 | $self->emit("unsubscribe", array($transport, $message)); 58 | $self->room($room)->emit("unsubscribe", array($transport, $message)); 59 | }); 60 | 61 | } elseif ($message->getEvent() == 'unsubscribe'){ 62 | $self->room($room)->unsubscribe($transport); 63 | } 64 | 65 | 66 | $self->room($room)->emit($message->getEvent(), array($transport, $message)); 67 | $self->emit($event, array($transport, $message)); 68 | }); 69 | } 70 | 71 | public function getRooms() 72 | { 73 | return array_keys($this->rooms); 74 | } 75 | } -------------------------------------------------------------------------------- /src/Devristo/Phpws/RemoteEvent/Room.php: -------------------------------------------------------------------------------- 1 | name = $name; 16 | $this->logger = $logger; 17 | } 18 | 19 | public function subscribe(StackTransport $transport) 20 | { 21 | $this->members[$transport->getId()] = $transport; 22 | $this->logger->notice("[{$this->name}] User {$transport->getId()} has subscribed!"); 23 | } 24 | 25 | public function unsubscribe(StackTransport $transport) 26 | { 27 | if (array_key_exists($transport->getId(), $this->members)){ 28 | unset($this->members[$transport->getId()]); 29 | 30 | $this->emit("unsubscribe", array($transport)); 31 | } 32 | } 33 | 34 | /** 35 | * @return StackTransport[] 36 | */ 37 | public function getMembers() 38 | { 39 | return array_values($this->members); 40 | } 41 | 42 | public function remoteEmit($event, $data){ 43 | foreach($this->getMembers() as $member){ 44 | $message = RemoteEventMessage::create($this->name, $event, $data); 45 | $member->getTopTransport()->send($message); 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /src/Devristo/Phpws/Server/OriginEnforcer.php: -------------------------------------------------------------------------------- 1 | on("handshake", function(Handshake $handshake) use ($allowedOrigins){ 15 | $originHeader = $handshake->getRequest()->getHeader('Origin', null); 16 | $origin = $originHeader ? $originHeader->getFieldValue() : null; 17 | 18 | if(in_array("*", $allowedOrigins) || !in_array($origin, $allowedOrigins)) 19 | $handshake->abort(); 20 | else{ 21 | // Confirm that the origin is allowed 22 | $handshake->getResponse()->getHeaders()->addHeaderLine("Access-Control-Allow-Origin", $origin); 23 | } 24 | }); 25 | } 26 | } -------------------------------------------------------------------------------- /src/Devristo/Phpws/Server/UriHandler/ClientRouter.php: -------------------------------------------------------------------------------- 1 | server = $server; 26 | $this->logger = $logger; 27 | $this->handlers = new \SplObjectStorage(); 28 | $this->membership = new \SplObjectStorage(); 29 | 30 | /** 31 | * @var $membership \SplObjectStorage|WebSocketUriHandlerInterface[] 32 | */ 33 | $membership = $this->membership; 34 | 35 | $that = $this; 36 | 37 | $server->on("connect", function(WebSocketTransportInterface $client) use ($that, $logger, $membership){ 38 | $handler = $that->matchConnection($client); 39 | 40 | if($handler){ 41 | $logger->notice("Added client {$client->getId()} to ".get_class($handler)); 42 | $membership->attach($client, $handler); 43 | $handler->emit("connect", array("client" => $client)); 44 | $handler->addConnection($client); 45 | }else 46 | $logger->err("Cannot route {$client->getId()} with request uri {$client->getHandshakeRequest()->getUriString()}"); 47 | }); 48 | 49 | $server->on('disconnect', function(WebSocketTransportInterface $client) use($that, $logger, $membership){ 50 | if($membership->contains($client)){ 51 | $handler = $membership[$client]; 52 | $membership->detach($client); 53 | 54 | $logger->notice("Removed client {$client->getId()} from".get_class($handler)); 55 | 56 | $handler->removeConnection($client); 57 | $handler->emit("disconnect", array("client" => $client)); 58 | 59 | } else { 60 | $logger->warn("Client {$client->getId()} not attached to any handler, so cannot remove it!"); 61 | } 62 | }); 63 | 64 | $server->on("message", function(WebSocketTransportInterface $client, WebSocketMessageInterface $message) use($that, $logger, $membership){ 65 | if($membership->contains($client)){ 66 | $handler = $membership[$client]; 67 | $handler->emit("message", compact('client', 'message')); 68 | } else { 69 | $logger->warn("Client {$client->getId()} not attached to any handler, so cannot forward the message!"); 70 | } 71 | }); 72 | } 73 | 74 | /** 75 | * @param \Devristo\Phpws\Protocol\WebSocketTransportInterface $transport 76 | * @return null|WebSocketUriHandlerInterface 77 | */ 78 | public function matchConnection(WebSocketTransportInterface $transport){ 79 | foreach($this->handlers as $tester){ 80 | if($tester($transport)) 81 | return $this->handlers[$tester]; 82 | } 83 | 84 | return null; 85 | } 86 | 87 | /** 88 | * @param string|callable $tester Either a regexp or a callable function: WebSocketTransportInterface -> boolean 89 | * @param WebSocketUriHandlerInterface $handler 90 | * @throws \InvalidArgumentException 91 | */ 92 | public function addRoute($tester, WebSocketUriHandlerInterface $handler){ 93 | if(is_string($tester)){ 94 | $tester = function(WebSocketTransportInterface $transport) use ($tester){ 95 | return preg_match($tester, $transport->getHandshakeRequest()->getUriString()); 96 | }; 97 | } elseif(!is_callable($tester)) 98 | throw new \InvalidArgumentException("Tester should either be a regexp or a callable"); 99 | 100 | $this->handlers->attach($tester, $handler); 101 | } 102 | } -------------------------------------------------------------------------------- /src/Devristo/Phpws/Server/UriHandler/WebSocketUriHandler.php: -------------------------------------------------------------------------------- 1 | users = new SplObjectStorage(); 37 | $this->logger = $logger; 38 | 39 | $this->on("message", array($this, 'onMessage')); 40 | $this->on("disconnect", array($this, 'onDisconnect')); 41 | $this->on("connect", array($this, 'onConnect')); 42 | } 43 | 44 | public function addConnection(WebSocketTransportInterface $user) 45 | { 46 | $this->users->attach($user); 47 | } 48 | 49 | public function removeConnection(WebSocketTransportInterface $user) 50 | { 51 | $this->users->detach($user); 52 | } 53 | 54 | public function onDisconnect(WebSocketTransportInterface $user) 55 | { 56 | 57 | } 58 | 59 | public function onConnect(WebSocketTransportInterface $user){ 60 | 61 | } 62 | 63 | public function onMessage(WebSocketTransportInterface $user, WebSocketMessageInterface $msg) 64 | { 65 | 66 | } 67 | 68 | /** 69 | * @return \Devristo\Phpws\Protocol\WebSocketTransportInterface[]|SplObjectStorage 70 | */ 71 | public function getConnections() 72 | { 73 | return $this->users; 74 | } 75 | 76 | } -------------------------------------------------------------------------------- /src/Devristo/Phpws/Server/UriHandler/WebSocketUriHandlerInterface.php: -------------------------------------------------------------------------------- 1 | \0"; 56 | 57 | /** 58 | * Handle incoming messages. 59 | * 60 | * Must be implemented by all extending classes 61 | * 62 | * @param $url 63 | * @param \React\EventLoop\LoopInterface $loop 64 | * @param \Zend\Log\LoggerInterface $logger 65 | * @throws \InvalidArgumentException 66 | */ 67 | public function __construct($url, LoopInterface $loop, LoggerInterface $logger) 68 | { 69 | $uri = new Uri($url); 70 | 71 | if($uri->getScheme() == 'ws') 72 | $uri->setScheme('tcp'); 73 | elseif($uri->getScheme() == 'wss') 74 | $uri->setScheme('ssl'); 75 | 76 | if($uri->getScheme() != 'tcp' && $uri->getScheme() != 'ssl') 77 | throw new \InvalidArgumentException("Uri scheme must be one of: tcp, ssl, ws, wss"); 78 | 79 | $this->uri = $uri; 80 | 81 | $this->loop = $loop; 82 | $this->_streams = new SplObjectStorage(); 83 | $this->_connections = new SplObjectStorage(); 84 | 85 | $this->_context = stream_context_create(); 86 | $this->_logger = $logger; 87 | } 88 | 89 | public function getStreamContext() 90 | { 91 | return $this->_context; 92 | } 93 | 94 | public function setStreamContext($context) 95 | { 96 | $this->_context = $context; 97 | } 98 | 99 | /** 100 | * Start the server 101 | */ 102 | public function bind() 103 | { 104 | 105 | $err = $errno = 0; 106 | 107 | $this->FLASH_POLICY_FILE = str_replace('to-ports="*', 'to-ports="' . $this->uri->getPort() ?: 80, $this->FLASH_POLICY_FILE); 108 | 109 | $serverSocket = stream_socket_server($this->uri->toString(), $errno, $err, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $this->_context); 110 | 111 | $this->_logger->notice(sprintf("phpws listening on %s", $this->uri->toString())); 112 | 113 | if ($serverSocket == false) { 114 | $this->_logger->err("Error: $err"); 115 | return; 116 | } 117 | 118 | $timeOut = & $this->purgeUserTimeOut; 119 | $sockets = $this->_streams; 120 | $that = $this; 121 | $logger = $this->_logger; 122 | 123 | $this->loop->addReadStream($serverSocket, function ($serverSocket) use ($that, $logger, $sockets) { 124 | try 125 | { 126 | $newSocket = stream_socket_accept($serverSocket); 127 | } catch (\ErrorException $e) { 128 | $newSocket = false; 129 | } 130 | 131 | if (false === $newSocket) { 132 | return; 133 | } 134 | 135 | stream_set_blocking($newSocket, 0); 136 | $client = new WebSocketConnection($newSocket, $that->loop, $logger); 137 | $sockets->attach($client); 138 | 139 | $client->on("handshake", function(Handshake $request) use($that, $client){ 140 | $that->emit("handshake",array($client->getTransport(), $request)); 141 | }); 142 | 143 | $client->on("connect", function () use ($that, $client, $logger) { 144 | $con = $client->getTransport(); 145 | $that->getConnections()->attach($con); 146 | $that->emit("connect", array("client" => $con)); 147 | }); 148 | 149 | $client->on("message", function ($message) use ($that, $client, $logger) { 150 | $connection = $client->getTransport(); 151 | $that->emit("message", array("client" => $connection, "message" => $message)); 152 | }); 153 | 154 | $client->on("close", function () use ($that, $client, $logger, &$sockets) { 155 | $sockets->detach($client); 156 | $connection = $client->getTransport(); 157 | 158 | if($connection){ 159 | $that->getConnections()->detach($connection); 160 | $that->emit("disconnect", array("client" => $connection)); 161 | } 162 | }); 163 | 164 | $client->on("flashXmlRequest", function () use ($that, $client) { 165 | $client->getTransport()->sendString($that->FLASH_POLICY_FILE); 166 | $client->close(); 167 | }); 168 | }); 169 | 170 | $this->loop->addPeriodicTimer(5, function () use ($timeOut, $sockets, $that) { 171 | 172 | # Lets send some pings 173 | foreach($that->getConnections() as $c){ 174 | if($c instanceof WebSocketTransportHybi) 175 | $c->sendFrame(WebSocketFrame::create(WebSocketOpcode::PingFrame)); 176 | } 177 | 178 | $currentTime = time(); 179 | if ($timeOut == null) 180 | return; 181 | 182 | foreach ($sockets as $s) { 183 | if ($currentTime - $s->getLastChanged() > $timeOut) { 184 | $s->close(); 185 | } 186 | } 187 | }); 188 | } 189 | 190 | public function getConnections() 191 | { 192 | return $this->_connections; 193 | } 194 | } 195 | 196 | -------------------------------------------------------------------------------- /tests/framing.test.php: -------------------------------------------------------------------------------- 1 | encode(); 20 | 21 | $this->assertEquals($bin, $enc); 22 | } 23 | 24 | function test_127chars() { 25 | 26 | $str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; 27 | 28 | $msg = WebSocketMessage::create($str); 29 | 30 | $serialised = ''; 31 | foreach ($msg->getFrames() as $frame) { 32 | $serialised .= $frame->encode(); 33 | } 34 | 35 | $frame = WebSocketFrame::decode($serialised); 36 | 37 | $this->assertEquals($str, $frame->getData()); 38 | } 39 | 40 | function test_maskedTextMessage() { 41 | $bin = "\x81\x85\x37\xfa\x21\x3d\x7f\x9f\x4d\x51\x58"; 42 | $str = "Hello"; 43 | 44 | $f = WebSocketFrame::decode($bin); 45 | 46 | $this->assertEquals($str, $f->getData()); 47 | } 48 | 49 | /** 50 | * @expectedException \Devristo\Phpws\Exceptions\WebSocketMessageNotFinalised 51 | */ 52 | function test_incompleteTextMessage() { 53 | $bf1 = "\x01\x03\x48\x65\x6c"; 54 | 55 | $f1 = WebSocketFrame::decode($bf1); 56 | 57 | $msg = WebSocketMessage::fromFrame($f1); 58 | 59 | $this->assertFalse($msg->isFinalised()); 60 | $msg->getData(); 61 | } 62 | 63 | function test_fragmentedTextMessage() { 64 | $bf1 = "\x01\x03\x48\x65\x6c"; 65 | $bf2 = "\x80\x02\x6c\x6f"; 66 | 67 | $f1 = WebSocketFrame::decode($bf1); 68 | $f2 = WebSocketFrame::decode($bf2); 69 | 70 | $this->assertEquals("Hel", $f1->getData()); 71 | $this->assertEquals("lo", $f2->getData()); 72 | 73 | $msg = WebSocketMessage::fromFrame($f1); 74 | $msg->takeFrame($f2); 75 | 76 | $this->assertEquals("Hello", $msg->getData()); 77 | } 78 | 79 | } 80 | 81 | -------------------------------------------------------------------------------- /tests/server.test.php: -------------------------------------------------------------------------------- 1 | setMasked(true); 22 | 23 | 24 | 25 | $client = new WebSocket("wss://127.0.0.1:12345/echo/"); 26 | $client->open(); 27 | $client->sendFrame($frame); 28 | 29 | $msg = $client->readMessage(); 30 | 31 | $client->close(); 32 | $this->assertEquals($input, $frame->getData()); 33 | $this->assertEquals(false, $msg->getFrames()[0]->isMasked()); 34 | } 35 | 36 | function test_echoResourceHandlerResponse() { 37 | $input = "Hello World!"; 38 | $msg = WebSocketMessage::create($input); 39 | 40 | $client = new WebSocket("wss://127.0.0.1:12345/echo/"); 41 | $client->open(); 42 | $client->sendMessage($msg); 43 | 44 | $msg = $client->readMessage(); 45 | 46 | $client->close(); 47 | $this->assertEquals($input, $msg->getData()); 48 | $this->assertEquals(false, $msg->getFrames()[0]->isMasked()); 49 | } 50 | 51 | function test_DoubleEchoResourceHandlerResponse() { 52 | $input = str_repeat("a", 1024); 53 | $input2 = str_repeat("b", 1024); 54 | $msg = WebSocketMessage::create($input); 55 | 56 | $client = new WebSocket("wss://127.0.0.1:12345/echo/"); 57 | $client->setTimeOut(1000); 58 | $client->open(); 59 | $client->sendMessage($msg); 60 | $client->sendMessage(WebSocketMessage::create($input2)); 61 | 62 | $msg = $client->readMessage(); 63 | $msg2 = $client->readMessage(); 64 | 65 | $client->close(); 66 | $this->assertEquals($input, $msg->getData()); 67 | 68 | $this->assertEquals($input2, $msg2->getData()); 69 | } 70 | 71 | } --------------------------------------------------------------------------------