├── Auth ├── AuthenticationProvider.php └── AuthenticationProviderInterface.php ├── Command └── StartCommand.php ├── DependencyInjection ├── Compiler │ └── ServerProviderCompilerPass.php ├── Configuration.php └── KrakenCollectiveWsSymfonyExtension.php ├── Dispatcher └── ClientEventDispatcher.php ├── Event ├── ClientErrorEvent.php ├── ClientEvent.php ├── ClientMessageEvent.php └── Events.php ├── Exception ├── RuntimeException.php ├── ServiceNotFoundException.php └── WsSymfonyException.php ├── Helper └── ConnectionHelper.php ├── KrakenCollectiveWsSymfonyBundle.php ├── LICENSE ├── README.md ├── Resources ├── config │ ├── parameters.yml │ └── services.yml ├── doc │ ├── config-reference.md │ └── index.md └── meta │ └── LICENSE ├── Server ├── Provider │ ├── AbstractProvider.php │ ├── ServerConfigProvider.php │ └── ServerProvider.php ├── Server.php ├── ServerComponent.php └── ServerConfig.php ├── Session └── SessionProvider.php └── composer.json /Auth/AuthenticationProvider.php: -------------------------------------------------------------------------------- 1 | tokenStorage = $tokenStorage; 36 | $this->firewalls = $firewalls; 37 | $this->allowAnonymous = $allowAnonymous; 38 | $this->hash = spl_object_hash($tokenStorage); 39 | } 40 | 41 | /** 42 | * @param NetworkConnectionInterface $conn 43 | * @return TokenInterface 44 | * @throws AccessDeniedException 45 | */ 46 | public function auth(NetworkConnectionInterface $conn) 47 | { 48 | $token = null; 49 | 50 | if (isset($conn->Session) && $conn->Session) { 51 | foreach ($this->firewalls as $firewall) { 52 | if (false !== $serializedToken = $conn->Session->get('_security_' . $firewall, false)) { 53 | $token = unserialize($serializedToken); 54 | break; 55 | } 56 | } 57 | } 58 | 59 | if (null === $token && $this->allowAnonymous) { 60 | $token = new AnonymousToken($this->firewalls[0], 'anon-' . $conn->getResourceId()); 61 | } 62 | 63 | if (null === $token) { 64 | throw new AccessDeniedException("Invalid credentials passed"); 65 | } 66 | 67 | if ($this->tokenStorage->getToken() !== $token) { 68 | $this->tokenStorage->setToken($token); 69 | } 70 | 71 | return $token; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Auth/AuthenticationProviderInterface.php: -------------------------------------------------------------------------------- 1 | setName('kraken:ws:start') 18 | ->addArgument('server', InputArgument::REQUIRED, 'Name of the server to run.'); 19 | } 20 | 21 | /** 22 | * @param InputInterface $input 23 | * @param OutputInterface $output 24 | * 25 | * @return void 26 | */ 27 | protected function execute(InputInterface $input, OutputInterface $output) 28 | { 29 | $serverName = $input->getArgument('server'); 30 | $serverProvider = $this->getContainer()->get('kraken.ws.server_provider'); 31 | $server = $serverProvider->getServer($serverName); 32 | 33 | $server->start(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /DependencyInjection/Compiler/ServerProviderCompilerPass.php: -------------------------------------------------------------------------------- 1 | getServiceIds(); 28 | 29 | foreach ($serviceIds as $serviceId) { 30 | if (!$container->hasDefinition($serviceId)) { 31 | $notFound[] = $serviceId; 32 | continue; 33 | } 34 | 35 | $def = $container->getDefinition($serviceId); 36 | if (!$def->isShared()) { 37 | $nonShared[] = $def; 38 | } 39 | } 40 | 41 | 42 | try { 43 | $serverProviderDefinition = $container->getDefinition(self::PARAMETER_SERVER_PROVIDER_ID); 44 | $serverConfigProviderDefinition = $container->getDefinition(self::PARAMETER_SERVER_CONFIG_PROVIDER_ID); 45 | 46 | $this->registerServicesInProvider( 47 | $container, 48 | $serverProviderDefinition, 49 | KrakenCollectiveWsSymfonyExtension::TAG_SERVER 50 | ); 51 | $this->registerServicesInProvider( 52 | $container, 53 | $serverConfigProviderDefinition, 54 | KrakenCollectiveWsSymfonyExtension::TAG_SERVER_CONFIG 55 | ); 56 | } catch (ServiceNotFoundException $e) { 57 | return; 58 | } 59 | } 60 | 61 | /** 62 | * @param ContainerBuilder $container 63 | * @param Definition $providerDefinition 64 | * @param string $tag 65 | * 66 | * @return void 67 | */ 68 | private function registerServicesInProvider(ContainerBuilder $container, Definition $providerDefinition, $tag) 69 | { 70 | $serviceIds = $container->findTaggedServiceIds($tag); 71 | 72 | foreach ($serviceIds as $serviceId => $tags) { 73 | foreach ($tags as $tag) { 74 | if (isset($tag['alias'])) { 75 | $providerDefinition->addMethodCall( 76 | self::METHOD_REGISTER_SERVICE, 77 | [$tag['alias'], $serviceId] 78 | ); 79 | } 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | root('kraken_collective_ws_symfony'); 22 | 23 | $rootNode 24 | ->append($this->getServerNode()) 25 | ->append($this->getSocketListenerNode()) 26 | ; 27 | 28 | return $treeBuilder; 29 | } 30 | 31 | private function getServerNode() 32 | { 33 | $builder = new TreeBuilder(); 34 | $node = $builder->root('server'); 35 | 36 | $node 37 | ->isRequired() 38 | ->requiresAtLeastOneElement() 39 | ->useAttributeAsKey('name') 40 | ->prototype('array') 41 | ->info('The server name is going to be used to identify this server.') 42 | ->children() 43 | ->scalarNode('listener') 44 | ->isRequired() 45 | ->info('Name of a defined socket_listener.') 46 | ->end() 47 | ->scalarNode('session_handler') 48 | ->defaultValue('session.handler') 49 | ->info('ID of a session handler. WARNING: Native session handlers will not work here. Use external provider, e.g. Redis.') 50 | ->end() 51 | ->arrayNode('authentication') 52 | ->children() 53 | ->arrayNode('firewalls') 54 | ->info('Names of the firewalls that the User is going to be authenticated against.') 55 | ->defaultValue(['default']) 56 | ->requiresAtLeastOneElement() 57 | ->prototype('scalar')->end() 58 | ->end() 59 | ->booleanNode('allow_anonymous') 60 | ->defaultValue(false) 61 | ->info('Switch to allow anonymous Users access.') 62 | ->end() 63 | ->end() 64 | ->end() 65 | ->arrayNode('routes') 66 | ->info('Public paths that allow connection to the server.') 67 | ->requiresAtLeastOneElement() 68 | ->prototype('scalar')->end() 69 | ->end() 70 | ->end() 71 | ->end() 72 | ; 73 | 74 | return $node; 75 | } 76 | 77 | private function getSocketListenerNode() 78 | { 79 | $builder = new TreeBuilder(); 80 | $node = $builder->root('socket_listener'); 81 | 82 | $node 83 | ->isRequired() 84 | ->requiresAtLeastOneElement() 85 | ->useAttributeAsKey('name') 86 | ->prototype('array') 87 | ->info('The socket_listener name is going to be used to identify this listener.') 88 | ->children() 89 | ->scalarNode('protocol') 90 | ->defaultValue('tcp') 91 | ->end() 92 | ->scalarNode('host') 93 | ->isRequired() 94 | ->end() 95 | ->integerNode('port') 96 | ->isRequired() 97 | ->end() 98 | ->enumNode('loop') 99 | ->defaultValue('select_loop') 100 | ->values(['select_loop']) 101 | ->info('Event loop model that should be used for this listener. Currently only "select_loop" is available.') 102 | ->end() 103 | ->end() 104 | ->end() 105 | ; 106 | 107 | return $node; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /DependencyInjection/KrakenCollectiveWsSymfonyExtension.php: -------------------------------------------------------------------------------- 1 | load('parameters.yml'); 64 | $loader->load('services.yml'); 65 | 66 | $this->mergedConfig = $mergedConfig; 67 | } 68 | 69 | /** 70 | * {@inheritdoc} 71 | */ 72 | public function process(ContainerBuilder $container) 73 | { 74 | $this->loadLoops($container); 75 | $this->loadSocketListeners($container, $this->mergedConfig[self::CONFIG_SOCKET_LISTENER]); 76 | $this->loadServers( 77 | $container, 78 | $this->mergedConfig[self::CONFIG_SERVER], 79 | $this->mergedConfig[self::CONFIG_SOCKET_LISTENER] 80 | ); 81 | } 82 | 83 | /** 84 | * @param ContainerBuilder $container 85 | * 86 | * @return void 87 | */ 88 | private function loadLoops(ContainerBuilder $container) 89 | { 90 | $loopModelsIds = $container->findTaggedServiceIds(self::TAG_LOOP_MODEL); 91 | 92 | foreach ($loopModelsIds as $loopModelId => $tags) { 93 | foreach ($tags as $tag) { 94 | if (isset($tag['alias'])) { 95 | $this->registerLoop( 96 | $container, 97 | $tag['alias'], 98 | $loopModelId 99 | ); 100 | } 101 | } 102 | } 103 | } 104 | 105 | /** 106 | * @param ContainerBuilder $container 107 | * @param string $loopAlias 108 | * @param string $loopModelId 109 | */ 110 | private function registerLoop( 111 | ContainerBuilder $container, 112 | $loopAlias, 113 | $loopModelId 114 | ) { 115 | $definition = new Definition($container->getParameter(self::LOOP_CLASS_PARAM)); 116 | $definition->addArgument(new Reference($loopModelId)); 117 | 118 | $container->setDefinition( 119 | $this->getLoopServiceId($loopAlias), 120 | $definition 121 | ); 122 | } 123 | 124 | /** 125 | * @param ContainerBuilder $container 126 | * @param array $configs 127 | * 128 | * @return void 129 | */ 130 | private function loadSocketListeners(ContainerBuilder $container, array $configs) 131 | { 132 | foreach ($configs as $socketListenerName => $config) { 133 | $this->registerSocketListener($container, $socketListenerName, $config); 134 | } 135 | } 136 | 137 | /** 138 | * @param ContainerBuilder $container 139 | * @param string $socketListenerName 140 | * @param array $config 141 | * 142 | * @return void 143 | */ 144 | private function registerSocketListener(ContainerBuilder $container, $socketListenerName, array $config) 145 | { 146 | $definition = new Definition($container->getParameter(self::SOCKET_LISTENER_CLASS_PARAM)); 147 | 148 | $definition->addArgument(sprintf('%s://%s:%s', $config['protocol'], $config['host'], $config['port'])); 149 | $definition->addArgument(new Reference($this->getLoopServiceId($config['loop']))); 150 | $definition->setPublic(false); 151 | 152 | $container->setDefinition( 153 | $this->getServiceId(self::SOCKET_LISTENER_ID_PREFIX, $socketListenerName), 154 | $definition 155 | ); 156 | } 157 | 158 | /** 159 | * @param ContainerBuilder $container 160 | * @param array $configs 161 | * @param array $listenersConfig 162 | * 163 | * @return void 164 | */ 165 | private function loadServers(ContainerBuilder $container, array $configs, array $listenersConfig) 166 | { 167 | foreach ($configs as $serverName => $config) { 168 | $this->loadServer($container, $serverName, $config, $listenersConfig); 169 | } 170 | } 171 | 172 | /** 173 | * @param ContainerBuilder $container 174 | * @param string $serverName 175 | * @param array $config 176 | * @param array $listenersConfig 177 | */ 178 | private function loadServer(ContainerBuilder $container, $serverName, array $config, array $listenersConfig) 179 | { 180 | $this->registerAuthenticationProvider( 181 | $container, 182 | $config['authentication'], 183 | $serverName 184 | ); 185 | 186 | $this->registerServerComponent( 187 | $container, 188 | $serverName 189 | ); 190 | 191 | $this->registerSessionProvider( 192 | $container, 193 | $config['session_handler'], 194 | $serverName 195 | ); 196 | 197 | $this->registerWebsocketServer( 198 | $container, 199 | $serverName 200 | ); 201 | 202 | $this->registerNetworkServer( 203 | $container, 204 | $config['listener'], 205 | $config['routes'], 206 | $serverName 207 | ); 208 | 209 | $this->registerServerConfig( 210 | $container, 211 | $listenersConfig[$config['listener']], 212 | $serverName 213 | ); 214 | 215 | $this->registerServer( 216 | $container, 217 | $this->getLoopServiceIdFromSocketListener($container, $config['listener']), 218 | $config['listener'], 219 | $serverName 220 | ); 221 | } 222 | 223 | /** 224 | * @param ContainerBuilder $container 225 | * @param array $config 226 | * @param string $serverName 227 | */ 228 | private function registerAuthenticationProvider(ContainerBuilder $container, array $config, $serverName) 229 | { 230 | $definition = new Definition($container->getParameter(self::AUTHENTICATION_PROVIDER_CLASS_PARAM)); 231 | $definition->addArgument(new Reference('security.token_storage')); 232 | $definition->addArgument($config['firewalls']); 233 | $definition->addArgument($config['allow_anonymous']); 234 | $definition->setPublic(false); 235 | 236 | $container->setDefinition( 237 | $this->getServiceId(self::AUTHENTICATION_PROVIDER_ID_PREFIX, $serverName), 238 | $definition 239 | ); 240 | } 241 | 242 | /** 243 | * @param ContainerBuilder $container 244 | * @param string $serverName 245 | */ 246 | private function registerServerComponent( 247 | ContainerBuilder $container, 248 | $serverName 249 | ) { 250 | $definition = new Definition($container->getParameter(self::SERVER_COMPONENT_CLASS_PARAM)); 251 | $definition->addArgument(new Reference('kraken.ws.dispatcher.client_event')); 252 | $definition->addArgument( 253 | new Reference($this->getServiceId(self::AUTHENTICATION_PROVIDER_ID_PREFIX, $serverName)) 254 | ); 255 | $definition->setPublic(false); 256 | 257 | $container->setDefinition( 258 | $this->getServiceId(self::SERVER_COMPONENT_ID_PREFIX, $serverName), 259 | $definition 260 | ); 261 | } 262 | 263 | /** 264 | * @param ContainerBuilder $container 265 | * @param string $sessionHandlerId 266 | * @param string $serverName 267 | */ 268 | private function registerSessionProvider( 269 | ContainerBuilder $container, 270 | $sessionHandlerId, 271 | $serverName 272 | ) { 273 | $definition = new Definition($container->getParameter(self::SESSION_PROVIDER_CLASS_PARAM)); 274 | $definition->addArgument(null); 275 | $definition->addArgument(new Reference($this->getServiceId(self::SERVER_COMPONENT_ID_PREFIX, $serverName))); 276 | $definition->addArgument(new Reference($sessionHandlerId)); 277 | $definition->setPublic(false); 278 | 279 | $container->setDefinition( 280 | $this->getServiceId(self::SESSION_PROVIDER_ID_PREFIX, $serverName), 281 | $definition 282 | ); 283 | } 284 | 285 | /** 286 | * @param ContainerBuilder $container 287 | * @param string $serverName 288 | */ 289 | private function registerWebsocketServer( 290 | ContainerBuilder $container, 291 | $serverName 292 | ) { 293 | $definition = new Definition($container->getParameter(self::WEBSOCKET_SERVER_CLASS_PARAM)); 294 | $definition->addArgument(null); 295 | $definition->addArgument(new Reference($this->getServiceId(self::SESSION_PROVIDER_ID_PREFIX, $serverName))); 296 | 297 | $container->setDefinition( 298 | $this->getServiceId(self::WEBSOCKET_SERVER_ID_PREFIX, $serverName), 299 | $definition 300 | ); 301 | } 302 | 303 | /** 304 | * @param ContainerBuilder $container 305 | * @param string $listenerAlias 306 | * @param array $routes 307 | * @param string $serverName 308 | */ 309 | private function registerNetworkServer( 310 | ContainerBuilder $container, 311 | $listenerAlias, 312 | array $routes, 313 | $serverName 314 | ) { 315 | $definition = new Definition($container->getParameter(self::NETWORK_SERVER_CLASS_PARAM)); 316 | $definition->addArgument(new Reference($this->getServiceId(self::SOCKET_LISTENER_ID_PREFIX, $listenerAlias))); 317 | 318 | foreach ($routes as $route) { 319 | $definition->addMethodCall( 320 | 'addRoute', 321 | [ 322 | $route, 323 | new Reference($this->getServiceId(self::WEBSOCKET_SERVER_ID_PREFIX, $serverName)) 324 | ] 325 | ); 326 | } 327 | 328 | $container->setDefinition( 329 | $this->getServiceId(self::NETWORK_SERVER_ID_PREFIX, $serverName), 330 | $definition 331 | ); 332 | } 333 | 334 | /** 335 | * @param ContainerBuilder $container 336 | * @param array $listenerConfig 337 | * @param string $serverName 338 | */ 339 | private function registerServerConfig(ContainerBuilder $container, array $listenerConfig, $serverName) 340 | { 341 | $definition = new Definition($container->getParameter(self::SERVER_CONFIG_CLASS_PARAM)); 342 | $definition->addArgument($listenerConfig['protocol']); 343 | $definition->addArgument($listenerConfig['host']); 344 | $definition->addArgument($listenerConfig['port']); 345 | $definition->addTag(self::TAG_SERVER_CONFIG, ['alias' => $serverName]); 346 | 347 | $container->setDefinition( 348 | $this->getServiceId(self::SERVER_CONFIG_ID_PREFIX, $serverName), 349 | $definition 350 | ); 351 | } 352 | 353 | /** 354 | * @param ContainerBuilder $container 355 | * @param Reference $loopReference 356 | * @param string $listenerAlias 357 | * @param string $serverName 358 | */ 359 | private function registerServer( 360 | ContainerBuilder $container, 361 | Reference $loopReference, 362 | $listenerAlias, 363 | $serverName 364 | ) { 365 | $definition = new Definition($container->getParameter(self::SERVER_CLASS_PARAM)); 366 | $definition->addArgument($loopReference); 367 | $definition->addArgument(new Reference($this->getServiceId(self::SOCKET_LISTENER_ID_PREFIX, $listenerAlias))); 368 | $definition->addArgument(new Reference($this->getServiceId(self::NETWORK_SERVER_ID_PREFIX, $serverName))); 369 | $definition->addTag(self::TAG_SERVER, ['alias' => $serverName]); 370 | 371 | $container->setDefinition( 372 | $this->getServiceId(self::SERVER_ID_PREFIX, $serverName), 373 | $definition 374 | ); 375 | } 376 | 377 | /** 378 | * @param ContainerBuilder $container 379 | * @param string $socketListenerName 380 | * @return string 381 | */ 382 | private function getLoopServiceIdFromSocketListener(ContainerBuilder $container, $socketListenerName) 383 | { 384 | $definition = $container->getDefinition( 385 | $this->getServiceId(self::SOCKET_LISTENER_ID_PREFIX, $socketListenerName) 386 | ); 387 | 388 | return $definition->getArgument(1); 389 | } 390 | 391 | /** 392 | * @param string $loopAlias 393 | * @return string 394 | */ 395 | private function getLoopServiceId($loopAlias) 396 | { 397 | return $this->getServiceId(self::LOOP_ID_PREFIX, $loopAlias); 398 | } 399 | 400 | /** 401 | * @param string $servicePrefix 402 | * @param string $serviceAlias 403 | * @return string 404 | */ 405 | private function getServiceId($servicePrefix, $serviceAlias) 406 | { 407 | return sprintf('%s.%s', $this->getFullServicePrefix($servicePrefix), $serviceAlias); 408 | } 409 | 410 | /** 411 | * @param string $servicePrefix 412 | * @return string 413 | */ 414 | private function getFullServicePrefix($servicePrefix) 415 | { 416 | return sprintf('%s.%s', self::ID_VENDOR_PREFIX, $servicePrefix); 417 | } 418 | } 419 | -------------------------------------------------------------------------------- /Dispatcher/ClientEventDispatcher.php: -------------------------------------------------------------------------------- 1 | eventDispatcher = $eventDispatcher; 19 | } 20 | 21 | /** 22 | * @param ClientEvent $event 23 | * 24 | * @return void 25 | */ 26 | public function dispatchClientConnectEvent(ClientEvent $event) 27 | { 28 | $this->eventDispatcher->dispatch(Events::CLIENT_CONNECTED, $event); 29 | } 30 | 31 | /** 32 | * @param ClientEvent $event 33 | * 34 | * @return void 35 | */ 36 | public function dispatchClientDisconnectEvent(ClientEvent $event) 37 | { 38 | $this->eventDispatcher->dispatch(Events::CLIENT_DISCONNECTED, $event); 39 | } 40 | 41 | /** 42 | * @param ClientEvent $event 43 | * 44 | * @return void 45 | */ 46 | public function dispatchClientMessageEvent(ClientEvent $event) 47 | { 48 | $this->eventDispatcher->dispatch(Events::CLIENT_MESSAGE, $event); 49 | } 50 | 51 | /** 52 | * @param ClientEvent $event 53 | * 54 | * @return void 55 | */ 56 | public function dispatchClientErrorEvent(ClientEvent $event) 57 | { 58 | $this->eventDispatcher->dispatch(Events::CLIENT_ERROR, $event); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Event/ClientErrorEvent.php: -------------------------------------------------------------------------------- 1 | ex = $ex; 21 | } 22 | 23 | /** 24 | * @return Error|Exception 25 | */ 26 | public function getThrowable() 27 | { 28 | return $this->ex; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Event/ClientEvent.php: -------------------------------------------------------------------------------- 1 | conn = $conn; 47 | $this->type = $type; 48 | } 49 | 50 | /** 51 | * @return int 52 | */ 53 | public function getType() 54 | { 55 | return $this->type; 56 | } 57 | 58 | /** 59 | * @return NetworkConnectionInterface 60 | */ 61 | public function getConnection() 62 | { 63 | return $this->conn; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Event/ClientMessageEvent.php: -------------------------------------------------------------------------------- 1 | message = $message; 20 | } 21 | 22 | /** 23 | * @return NetworkMessageInterface 24 | */ 25 | public function getMessage() 26 | { 27 | return $this->message; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Event/Events.php: -------------------------------------------------------------------------------- 1 | getHost(), 22 | $serverConfig->getPort(), 23 | $route, 24 | $token 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /KrakenCollectiveWsSymfonyBundle.php: -------------------------------------------------------------------------------- 1 | addCompilerPass(new ServerProviderCompilerPass(), PassConfig::TYPE_OPTIMIZE); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Kraken Collective 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WsSymfonyBundle # 2 | 3 | ## About ## 4 | 5 | This bundle integrates [Kraken Framework Network Component](https://github.com/kraken-php/network) into your Symfony application. 6 | 7 | ## Branches ## 8 | 9 | * Use version `1.0.*` or the `1.0` branch. The bundle requires Symfony `3.0` or higher. 10 | 11 | This bundle is also available via [composer](https://github.com/composer/composer), find it on [packagist](http://packagist.org/packages/kraken-collective/ws-symfony-bundle). 12 | 13 | ## Documentation ## 14 | 15 | [Read the documentation in Resources/doc/](https://github.com/kraken-collective/ws-symfony/blob/master/Resources/doc/index.md) 16 | 17 | ## Demo application 18 | 19 | To see WsSymfonyBundle in action clone this [example app repository](https://github.com/kraken-collective/ws-symfony-example). It's a simple chat application based on WebSocket communication between clients and the server. 20 | 21 | ## License ## 22 | 23 | See [Resources/meta/LICENSE](https://github.com/kraken-collective/ws-symfony/blob/master/Resources/meta/LICENSE). 24 | -------------------------------------------------------------------------------- /Resources/config/parameters.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | # Service classes definitions: 3 | kraken_collective.ws_symfony.loop: Kraken\Loop\Loop 4 | kraken_collective.ws_symfony.select_loop: Kraken\Loop\Model\SelectLoop 5 | kraken_collective.ws_symfony.socket_listener: Kraken\Ipc\Socket\SocketListener 6 | kraken_collective.ws_symfony.network_server: Kraken\Network\NetworkServer 7 | kraken_collective.ws_symfony.websocket_server: Kraken\Network\Websocket\WsServer 8 | kraken_collective.ws_symfony.session_provider: KrakenCollective\WsSymfonyBundle\Session\SessionProvider 9 | kraken_collective.ws_symfony.authentication_provider: KrakenCollective\WsSymfonyBundle\Auth\AuthenticationProvider 10 | kraken_collective.ws_symfony.server_component: KrakenCollective\WsSymfonyBundle\Server\ServerComponent 11 | kraken_collective.ws_symfony.server: KrakenCollective\WsSymfonyBundle\Server\Server 12 | kraken_collective.ws_symfony.server_config: KrakenCollective\WsSymfonyBundle\Server\ServerConfig 13 | kraken_collective.ws_symfony.server_provider: KrakenCollective\WsSymfonyBundle\Server\Provider\ServerProvider 14 | kraken_collective.ws_symfony.server_config_provider: KrakenCollective\WsSymfonyBundle\Server\Provider\ServerConfigProvider 15 | kraken_collective.ws_symfony.client_event_dispatcher: KrakenCollective\WsSymfonyBundle\Dispatcher\ClientEventDispatcher 16 | kraken_collective.ws_symfony.connection_helper: KrakenCollective\WsSymfonyBundle\Helper\ConnectionHelper 17 | -------------------------------------------------------------------------------- /Resources/config/services.yml: -------------------------------------------------------------------------------- 1 | services: 2 | kraken.ws.server_provider: 3 | class: "%kraken_collective.ws_symfony.server_provider%" 4 | calls: 5 | - ["setContainer", ["@service_container"]] 6 | 7 | kraken.ws.server_config_provider: 8 | class: "%kraken_collective.ws_symfony.server_config_provider%" 9 | calls: 10 | - ["setContainer", ["@service_container"]] 11 | 12 | kraken.ws.connection_helper: 13 | class: "%kraken_collective.ws_symfony.connection_helper%" 14 | 15 | kraken.ws.loop_model.select_loop: 16 | class: "%kraken_collective.ws_symfony.select_loop%" 17 | public: false 18 | tags: 19 | - { name: kraken.loop_model, alias: select_loop } 20 | 21 | kraken.ws.dispatcher.client_event: 22 | class: "%kraken_collective.ws_symfony.client_event_dispatcher%" 23 | public: false 24 | arguments: 25 | - "@event_dispatcher" 26 | -------------------------------------------------------------------------------- /Resources/doc/config-reference.md: -------------------------------------------------------------------------------- 1 | ```yaml 2 | # Default configuration for extension with alias: "kraken_collective_ws_symfony" 3 | kraken_collective_ws_symfony: 4 | server: # Required 5 | 6 | # Prototype: The server name is going to be used to identify this server. 7 | name: 8 | 9 | # Name of a defined socket_listener. 10 | listener: ~ # Required 11 | 12 | # ID of a session handler. WARNING: Native session handlers will not work here. Use external provider, e.g. Redis. 13 | session_handler: session.handler 14 | authentication: 15 | 16 | # Names of the firewalls that the User is going to be authenticated against. 17 | firewalls: 18 | 19 | # Default: 20 | - default 21 | 22 | # Switch to allow anonymous Users access. 23 | allow_anonymous: false 24 | 25 | # Public paths that allow connection to the server. 26 | routes: [] 27 | socket_listener: # Required 28 | 29 | # Prototype: The socket_listener name is going to be used to identify this listener. 30 | name: 31 | protocol: tcp 32 | host: ~ # Required 33 | port: ~ # Required 34 | 35 | # Event loop model that should be used for this listener. Currently only "select_loop" is available. 36 | loop: select_loop # One of "select_loop" 37 | ``` 38 | -------------------------------------------------------------------------------- /Resources/doc/index.md: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | Step 1: Download the Bundle 5 | --------------------------- 6 | 7 | Open a command console, enter your project directory and execute the 8 | following command to download the latest stable version of this bundle: 9 | 10 | ```console 11 | $ composer require kraken-collective/ws-symfony-bundle "~1" 12 | ``` 13 | 14 | This command requires you to have Composer installed globally, as explained 15 | in the [installation chapter](https://getcomposer.org/doc/00-intro.md) 16 | of the Composer documentation. 17 | 18 | Step 2: Enable the Bundle 19 | ------------------------- 20 | 21 | Then, enable the bundle by adding it to the list of registered bundles 22 | in the `app/AppKernel.php` file of your project: 23 | 24 | ```php 25 | 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Server/Provider/AbstractProvider.php: -------------------------------------------------------------------------------- 1 | guardAgainstMissingAlias($alias); 23 | 24 | return $this->returnService($alias); 25 | } 26 | 27 | /** 28 | * @param string $alias 29 | * 30 | * @return void 31 | * 32 | * @throws ServiceNotFoundException 33 | */ 34 | private function guardAgainstMissingAlias($alias) 35 | { 36 | if (!isset($this->services[$alias])) { 37 | throw new ServiceNotFoundException($alias); 38 | } 39 | } 40 | 41 | /** 42 | * @param string $alias 43 | * 44 | * @return object 45 | */ 46 | private function returnService($alias) 47 | { 48 | return $this->container->get( 49 | $this->services[$alias] 50 | ); 51 | } 52 | 53 | /** 54 | * @param string $alias 55 | * @param string $serviceId 56 | * 57 | * @return void 58 | */ 59 | public function registerService($alias, $serviceId) 60 | { 61 | $this->guardAgainstDuplicatedServiceAlias($alias); 62 | $this->addService($alias, $serviceId); 63 | } 64 | 65 | /** 66 | * @param string $alias 67 | * 68 | * @return void 69 | * 70 | * @throws RuntimeException 71 | */ 72 | private function guardAgainstDuplicatedServiceAlias($alias) 73 | { 74 | if (isset($this->services[$alias])) { 75 | throw new RuntimeException( 76 | sprintf( 77 | 'Server aliased "%s" ("%") has already been registered. You cannot have multiple servers with the same alias!', 78 | $alias, 79 | $this->services[$alias] 80 | ) 81 | ); 82 | } 83 | } 84 | 85 | /** 86 | * @param string $alias 87 | * @param string $serviceId 88 | * 89 | * @return void 90 | */ 91 | private function addService($alias, $serviceId) 92 | { 93 | $this->services[$alias] = $serviceId; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Server/Provider/ServerConfigProvider.php: -------------------------------------------------------------------------------- 1 | get($alias); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Server/Provider/ServerProvider.php: -------------------------------------------------------------------------------- 1 | get($alias); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Server/Server.php: -------------------------------------------------------------------------------- 1 | loop = $loop; 35 | $this->networkServer = $networkServer; 36 | $this->socketListener = $socketListener; 37 | } 38 | 39 | /** 40 | * This function starts the event loop. Once it's called, the server is ready to receive messages. 41 | * This function is blocking. 42 | */ 43 | public function start() 44 | { 45 | $this->loop->start(); 46 | } 47 | 48 | /** 49 | * Stops the event loop, effectively disabling the server. 50 | */ 51 | public function stop() 52 | { 53 | $this->loop->stop(); 54 | } 55 | 56 | /** 57 | * @return string 58 | */ 59 | public function getLocalAddress() 60 | { 61 | return $this->socketListener->getLocalAddress(); 62 | } 63 | 64 | /** 65 | * @return string 66 | */ 67 | public function getLocalHost() 68 | { 69 | return $this->socketListener->getLocalHost(); 70 | } 71 | 72 | /** 73 | * @return string 74 | */ 75 | public function getLocalPort() 76 | { 77 | return $this->socketListener->getLocalPort(); 78 | } 79 | 80 | /** 81 | * @return LoopExtendedInterface 82 | */ 83 | public function getLoop() 84 | { 85 | return $this->loop; 86 | } 87 | 88 | /** 89 | * @return NetworkServerInterface 90 | */ 91 | public function getNetworkServer() 92 | { 93 | return $this->networkServer; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Server/ServerComponent.php: -------------------------------------------------------------------------------- 1 | eventDispatcher = $eventDispatcher; 34 | $this->authenticationProvider = $authenticationProvider; 35 | } 36 | 37 | /** 38 | * @override 39 | * @inheritDoc 40 | */ 41 | public function handleConnect(NetworkConnectionInterface $conn) 42 | { 43 | if ($this->auth($conn)) { 44 | $event = new ClientEvent(ClientEvent::CONNECTED, $conn); 45 | $this->eventDispatcher->dispatchClientConnectEvent($event); 46 | } 47 | } 48 | 49 | /** 50 | * @override 51 | * @inheritDoc 52 | */ 53 | public function handleDisconnect(NetworkConnectionInterface $conn) 54 | { 55 | if ($this->auth($conn)) { 56 | $event = new ClientEvent(ClientEvent::DISCONNECTED, $conn); 57 | $this->eventDispatcher->dispatchClientDisconnectEvent($event); 58 | } 59 | } 60 | 61 | /** 62 | * @override 63 | * @inheritDoc 64 | */ 65 | public function handleMessage(NetworkConnectionInterface $conn, NetworkMessageInterface $message) 66 | { 67 | if ($this->auth($conn)) { 68 | $event = new ClientMessageEvent(ClientEvent::MESSAGE, $conn); 69 | $event->setMessage($message); 70 | $this->eventDispatcher->dispatchClientMessageEvent($event); 71 | } 72 | } 73 | 74 | /** 75 | * @override 76 | * @inheritDoc 77 | */ 78 | public function handleError(NetworkConnectionInterface $conn, $ex) 79 | { 80 | $event = new ClientErrorEvent(ClientEvent::ERROR, $conn); 81 | $event->setThrowable($ex); 82 | $this->eventDispatcher->dispatchClientErrorEvent($event); 83 | } 84 | 85 | /** 86 | * @param NetworkConnectionInterface $conn 87 | * @return bool 88 | */ 89 | private function auth(NetworkConnectionInterface $conn) 90 | { 91 | try { 92 | $this->authenticationProvider->auth($conn); 93 | return true; 94 | 95 | } catch (Error $ex) { 96 | $this->handleError($conn, $ex); 97 | 98 | } catch (Exception $ex) { 99 | $this->handleError($conn, $ex); 100 | } 101 | 102 | return false; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Server/ServerConfig.php: -------------------------------------------------------------------------------- 1 | protocol = $protocol; 23 | $this->host = $host; 24 | $this->port = $port; 25 | } 26 | 27 | /** 28 | * @return string 29 | */ 30 | public function getProtocol() 31 | { 32 | return $this->protocol; 33 | } 34 | 35 | /** 36 | * @return string 37 | */ 38 | public function getHost() 39 | { 40 | return $this->host; 41 | } 42 | 43 | /** 44 | * @return string 45 | */ 46 | public function getPort() 47 | { 48 | return $this->port; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Session/SessionProvider.php: -------------------------------------------------------------------------------- 1 | component = $component; 56 | $this->handler = $handler !== null ? $handler : new NullSessionHandler; 57 | $this->nullHandler = new NullSessionHandler(); 58 | 59 | ini_set('session.auto_start', 0); 60 | ini_set('session.cache_limiter', ''); 61 | ini_set('session.use_cookies', 0); 62 | 63 | $this->setOptions($options); 64 | 65 | if ($serializer === null) { 66 | $serialClass = "\\Ratchet\\Session\\Serialize\\{$this->toClassCase(ini_get('session.serialize_handler'))}Handler"; 67 | 68 | if (!class_exists($serialClass)) { 69 | throw new RuntimeException('Unable to parse session serialize handler.'); 70 | } 71 | 72 | $serializer = new $serialClass; 73 | } 74 | 75 | $this->serializer = $serializer; 76 | 77 | if ($aware !== null) { 78 | $aware->setComponent($this); 79 | } 80 | } 81 | 82 | /** 83 | * 84 | */ 85 | public function __destruct() 86 | { 87 | unset($this->component); 88 | unset($this->handler); 89 | unset($this->nullHandler); 90 | unset($this->serializer); 91 | } 92 | 93 | /** 94 | * @override 95 | * @inheritDoc 96 | */ 97 | public function setComponent(NetworkComponentInterface $component = null) 98 | { 99 | $this->component = $component === null ? new NullServer() : $component; 100 | } 101 | 102 | /** 103 | * @override 104 | * @inheritDoc 105 | */ 106 | public function getComponent() 107 | { 108 | return $this->component; 109 | } 110 | 111 | /** 112 | * @override 113 | * @inheritDoc 114 | */ 115 | public function handleConnect(NetworkConnectionInterface $conn) 116 | { 117 | $queryParts = explode('&', $conn->httpRequest->getUri()->getQuery()); 118 | $token = null; 119 | 120 | foreach ($queryParts as $queryPart) { 121 | if (stripos($queryPart, 'token=') === 0) { 122 | $token = substr($queryPart, 6); 123 | break; 124 | } 125 | } 126 | 127 | if ($token === null) { 128 | $token = ''; 129 | } 130 | 131 | $conn->Session = new Session(new VirtualSessionStorage($this->handler, $token, $this->serializer)); 132 | $conn->Session->start(); 133 | 134 | return $this->component->handleConnect($conn); 135 | } 136 | 137 | /** 138 | * @override 139 | * @inheritDoc 140 | */ 141 | public function handleDisconnect(NetworkConnectionInterface $conn) 142 | { 143 | return $this->component->handleDisconnect($conn); 144 | } 145 | 146 | /** 147 | * @override 148 | * @inheritDoc 149 | */ 150 | public function handleMessage(NetworkConnectionInterface $conn, NetworkMessageInterface $message) 151 | { 152 | return $this->component->handleMessage($conn, $message); 153 | } 154 | 155 | /** 156 | * @override 157 | * @inheritDoc 158 | */ 159 | public function handleError(NetworkConnectionInterface $conn, $ex) 160 | { 161 | return $this->component->handleError($conn, $ex); 162 | } 163 | 164 | /** 165 | * Set all the php session. ini options. 166 | * 167 | * @param string[] $options 168 | * @return string[] 169 | */ 170 | protected function setOptions($options) 171 | { 172 | $all = [ 173 | 'auto_start', 174 | 'cache_limiter', 175 | 'cookie_domain', 176 | 'cookie_httponly', 177 | 'cookie_lifetime', 178 | 'cookie_path', 179 | 'cookie_secure', 180 | 'entropy_file', 181 | 'entropy_length', 182 | 'gc_divisor', 183 | 'gc_maxlifetime', 184 | 'gc_probability', 185 | 'hash_bits_per_character', 186 | 'hash_function', 187 | 'name', 188 | 'referer_check', 189 | 'serialize_handler', 190 | 'use_cookies', 191 | 'use_only_cookies', 192 | 'use_trans_sid', 193 | 'upload_progress.enabled', 194 | 'upload_progress.cleanup', 195 | 'upload_progress.prefix', 196 | 'upload_progress.name', 197 | 'upload_progress.freq', 198 | 'upload_progress.min-freq', 199 | 'url_rewriter.tags' 200 | ]; 201 | 202 | foreach ($all as $key) 203 | { 204 | if (!array_key_exists($key, $options)) 205 | { 206 | $options[$key] = ini_get("session.{$key}"); 207 | } 208 | else 209 | { 210 | ini_set("session.{$key}", $options[$key]); 211 | } 212 | } 213 | 214 | return $options; 215 | } 216 | 217 | /** 218 | * @param string $langDef Input to convert 219 | * @return string 220 | */ 221 | protected function toClassCase($langDef) 222 | { 223 | return str_replace(' ', '', ucwords(str_replace('_', ' ', $langDef))); 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kraken-collective/ws-symfony-bundle", 3 | "type": "symfony-bundle", 4 | "description": "Symfony Websocket Bundle", 5 | "keywords": ["websocket", "kraken", "symfony", "bundle"], 6 | "homepage": "https://github.com/kraken-collective/ws-symfony", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Kamil Jamróz" 11 | }, 12 | { 13 | "name": "Michał Kurzeja", 14 | "email": "mike.kurzeja@gmail.com" 15 | } 16 | ], 17 | "require": { 18 | "php": "^5.5.9 || ^7.0", 19 | "symfony/framework-bundle": "^3.0", 20 | "symfony/console": "^3.0", 21 | "symfony/security-bundle": "^3.0", 22 | "kraken-php/network": "^0.3.2" 23 | }, 24 | "require-dev": { 25 | "symfony/phpunit-bridge": "^2.7 || ^3.0", 26 | "phpunit/phpunit": "~4.8|~5.0" 27 | }, 28 | "suggest": { 29 | "snc/redis-bundle": "Provides an alternative SessionHandler (native session handlers won't work with this bundle)." 30 | }, 31 | "autoload": { 32 | "psr-4": { 33 | "KrakenCollective\\WsSymfonyBundle\\": "" 34 | }, 35 | "exclude-from-classmap": [ 36 | "/Tests/" 37 | ] 38 | }, 39 | "autoload-dev": { 40 | "psr-4": { 41 | "KrakenCollective\\WsSymfonyBundle\\Tests\\": "Tests/" 42 | } 43 | } 44 | } 45 | --------------------------------------------------------------------------------