├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bugreport.yml │ └── config.yml └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── SECURITY.md ├── composer.json ├── phpunit.xml.dist ├── phpunit.xml.legacy ├── src └── Ratchet │ ├── AbstractConnectionDecorator.php │ ├── App.php │ ├── ComponentInterface.php │ ├── ConnectionInterface.php │ ├── Http │ ├── CloseResponseTrait.php │ ├── HttpRequestParser.php │ ├── HttpServer.php │ ├── HttpServerInterface.php │ ├── NoOpHttpServerController.php │ ├── OriginCheck.php │ └── Router.php │ ├── MessageComponentInterface.php │ ├── MessageInterface.php │ ├── Server │ ├── EchoServer.php │ ├── FlashPolicy.php │ ├── IoConnection.php │ ├── IoServer.php │ └── IpBlackList.php │ ├── Session │ ├── Serialize │ │ ├── HandlerInterface.php │ │ ├── PhpBinaryHandler.php │ │ └── PhpHandler.php │ ├── SessionProvider.php │ └── Storage │ │ ├── Proxy │ │ ├── VirtualProxy.php │ │ ├── VirtualProxyForSymfony6.php │ │ └── VirtualProxyForSymfony7.php │ │ ├── VirtualSessionStorage.php │ │ ├── VirtualSessionStorageForSymfony6.php │ │ └── VirtualSessionStorageForSymfony7.php │ ├── Wamp │ ├── Exception.php │ ├── JsonException.php │ ├── ServerProtocol.php │ ├── Topic.php │ ├── TopicManager.php │ ├── WampConnection.php │ ├── WampServer.php │ └── WampServerInterface.php │ └── WebSocket │ ├── ConnContext.php │ ├── MessageCallableInterface.php │ ├── MessageComponentInterface.php │ ├── WsConnection.php │ ├── WsServer.php │ └── WsServerInterface.php └── tests ├── autobahn ├── bin │ └── fuzzingserver.php ├── fuzzingclient-all.json ├── fuzzingclient-profile.json └── fuzzingclient-quick.json ├── bootstrap.php ├── helpers └── Ratchet │ ├── AbstractMessageComponentTestCase.php │ ├── Mock │ ├── Connection.php │ ├── ConnectionDecorator.php │ └── WampComponent.php │ ├── NullComponent.php │ ├── Wamp │ └── Stub │ │ └── WsWampServerInterface.php │ └── WebSocket │ └── Stub │ └── WsMessageComponentInterface.php └── unit ├── AbstractConnectionDecoratorTest.php ├── AppTest.php ├── Http ├── HttpRequestParserTest.php ├── HttpServerTest.php ├── OriginCheckTest.php └── RouterTest.php ├── Server ├── EchoServerTest.php ├── FlashPolicyComponentTest.php ├── IoConnectionTest.php ├── IoServerTest.php └── IpBlackListComponentTest.php ├── Session ├── Serialize │ ├── PhpBinaryHandlerTest.php │ └── PhpHandlerTest.php ├── SessionProviderTest.php └── Storage │ └── VirtualSessionStoragePDOTest.php └── Wamp ├── ServerProtocolTest.php ├── TopicManagerTest.php ├── TopicTest.php ├── WampConnectionTest.php └── WampServerTest.php /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: 2 | - ReactPHP 3 | - clue 4 | - cboden 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bugreport.yml: -------------------------------------------------------------------------------- 1 | name: "🐛 Bug Report" 2 | description: "Found a bug in our project? Create a report to help us improve." 3 | body: 4 | - type: markdown 5 | attributes: 6 | value: "Thanks for helping improve Ratchet by reporting a bug! 7 | Please check [existing Issues](https://github.com/ratchetphp/Ratchet/issues) before submitting." 8 | 9 | - type: textarea 10 | id: reproduction 11 | attributes: 12 | label: "Reproduction Steps" 13 | description: "Exact steps to reproduce (include code samples)" 14 | validations: 15 | required: true 16 | 17 | - type: textarea 18 | id: environment 19 | attributes: 20 | label: "Runtime Environment" 21 | placeholder: "PHP version, Ratchet version, etc." 22 | validations: 23 | required: true 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: "Ratchet Discussions" 4 | url: https://github.com/ratchetphp/Ratchet/discussions 5 | about: 'We are happy to answer your questions! Start a new discussion in our "Q&A" category.' 6 | - name: "Feature Requests" 7 | url: https://github.com/ratchetphp/Ratchet/discussions/categories/ideas 8 | about: 'You have ideas to improve our project? Start a new discussion in our "Ideas" category.' 9 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: "CI" 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | phpunit: 9 | name: "PHPUnit" 10 | runs-on: "ubuntu-24.04" 11 | 12 | strategy: 13 | matrix: 14 | php-version: 15 | - "5.4" 16 | - "5.5" 17 | - "5.6" 18 | - "7.0" 19 | - "7.1" 20 | - "7.2" 21 | - "7.3" 22 | - "7.4" 23 | - "8.0" 24 | - "8.1" 25 | - "8.2" 26 | - "8.3" 27 | - "8.4" 28 | dependencies: 29 | - "highest" 30 | include: 31 | - dependencies: "lowest" 32 | php-version: "5.4" 33 | 34 | steps: 35 | - name: "Checkout" 36 | uses: "actions/checkout@v4" 37 | with: 38 | fetch-depth: 2 39 | 40 | - name: "Install PHP" 41 | uses: "shivammathur/setup-php@v2" 42 | with: 43 | php-version: "${{ matrix.php-version }}" 44 | coverage: "none" 45 | ini-values: "zend.assertions=1" 46 | 47 | - name: "Install dependencies with Composer" 48 | uses: "ramsey/composer-install@v3" 49 | with: 50 | dependency-versions: "${{ matrix.dependencies }}" 51 | 52 | - name: "Run PHPUnit" 53 | run: "vendor/bin/phpunit ${{ matrix.php-version < 7.3 && ' -c phpunit.xml.legacy' || '' }}" 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | phpunit.xml 2 | reports 3 | sandbox 4 | vendor 5 | composer.lock -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ========= 3 | 4 | ### Legend 5 | 6 | * "BC": Backwards compatibility break (from public component APIs) 7 | * "BF": Bug fix 8 | 9 | --- 10 | 11 | * 0.4.4 (2021-12-11) 12 | * Correct and update dependencies for forward compatibility 13 | * Added context for React Socket server to App 14 | * Use non-deprecated Guzzle API calls 15 | 16 | * 0.4.3 (2020-06-04) 17 | * BF: Fixed interface acceptable regression in `App` 18 | * Update RFC6455 library with latest fixes 19 | 20 | * 0.4.2 (2020-01-27) 21 | * Support Symfony 5 22 | * BF: Use phpunit from vendor directory 23 | * Allow disabling of xdebug warning by defining `RATCHET_DISABLE_XDEBUG_WARN` 24 | * Stop using `LoopInterface::tick()` for testing 25 | 26 | * 0.4.1 (2017-12-11) 27 | * Only enableKeepAlive in App if no WsServer passed allowing user to set their own timeout duration 28 | * Support Symfony 4 29 | * BF: Plug NOOP controller in connection from router in case of misbehaving client 30 | * BF: Raise error from invalid WAMP payload 31 | 32 | * 0.4 (2017-09-14) 33 | * BC: $conn->WebSocket->request replaced with $conn->httpRequest which is a PSR-7 object 34 | * Binary messages now supported via Ratchet\WebSocket\MessageComponentInterface 35 | * Added heartbeat support via ping/pong in WsServer 36 | * BC: No longer support old (and insecure) Hixie76 and Hybi protocols 37 | * BC: No longer support disabling UTF-8 checks 38 | * BC: The Session component implements HttpServerInterface instead of WsServerInterface 39 | * BC: PHP 5.3 no longer supported 40 | * BC: Update to newer version of react/socket dependency 41 | * BC: WAMP topics reduced to 0 subscriptions are deleted, new subs to same name will result in new Topic instance 42 | * Significant performance enhancements 43 | 44 | * 0.3.6 (2017-01-06) 45 | * BF: Keep host and scheme in HTTP request object attatched to connection 46 | * BF: Return correct HTTP response (405) when non-GET request made 47 | 48 | * 0.3.5 (2016-05-25) 49 | * BF: Unmask responding close frame 50 | * Added write handler for PHP session serializer 51 | 52 | * 0.3.4 (2015-12-23) 53 | * BF: Edge case where version check wasn't run on message coalesce 54 | * BF: Session didn't start when using pdo_sqlite 55 | * BF: WAMP currie prefix check when using '#' 56 | * Compatibility with Symfony 3 57 | 58 | * 0.3.3 (2015-05-26) 59 | * BF: Framing bug on large messages upon TCP fragmentation 60 | * BF: Symfony Router query parameter defaults applied to Request 61 | * BF: WAMP CURIE on all URIs 62 | * OriginCheck rules applied to FlashPolicy 63 | * Switched from PSR-0 to PSR-4 64 | 65 | * 0.3.2 (2014-06-08) 66 | * BF: No messages after closing handshake (fixed rare race condition causing 100% CPU) 67 | * BF: Fixed accidental BC break from v0.3.1 68 | * Added autoDelete parameter to Topic to destroy when empty of connections 69 | * Exposed React Socket on IoServer (allowing FlashPolicy shutdown in App) 70 | * Normalized Exceptions in WAMP 71 | 72 | * 0.3.1 (2014-05-26) 73 | * Added query parameter support to Router, set in HTTP request (ws://server?hello=world) 74 | * HHVM compatibility 75 | * BF: React/0.4 support; CPU starvation bug fixes 76 | * BF: Allow App::route to ignore Host header 77 | * Added expected filters to WAMP Topic broadcast method 78 | * Resource cleanup in WAMP TopicManager 79 | 80 | * 0.3.0 (2013-10-14) 81 | * Added the `App` class to help making Ratchet so easy to use it's silly 82 | * BC: Require hostname to do HTTP Host header match and do Origin HTTP header check, verify same name by default, helping prevent CSRF attacks 83 | * Added Symfony/2.2 based HTTP Router component to allowing for a single Ratchet server to handle multiple apps -> Ratchet\Http\Router 84 | * BC: Decoupled HTTP from WebSocket component -> Ratchet\Http\HttpServer 85 | * BF: Single sub-protocol selection to conform with RFC6455 86 | * BF: Sanity checks on WAMP protocol to prevent errors 87 | 88 | * 0.2.8 (2013-09-19) 89 | * React 0.3 support 90 | 91 | * 0.2.7 (2013-06-09) 92 | * BF: Sub-protocol negotation with Guzzle 3.6 93 | 94 | * 0.2.6 (2013-06-01) 95 | * Guzzle 3.6 support 96 | 97 | * 0.2.5 (2013-04-01) 98 | * Fixed Hixie-76 handshake bug 99 | 100 | * 0.2.4 (2013-03-09) 101 | * Support for Symfony 2.2 and Guzzle 2.3 102 | * Minor bug fixes when handling errors 103 | 104 | * 0.2.3 (2012-11-21) 105 | * Bumped dep: Guzzle to v3, React to v0.2.4 106 | * More tests 107 | 108 | * 0.2.2 (2012-10-20) 109 | * Bumped deps to use React v0.2 110 | 111 | * 0.2.1 (2012-10-13) 112 | * BF: No more UTF-8 warnings in browsers (no longer sending empty sub-protocol string) 113 | * Documentation corrections 114 | * Using new composer structure 115 | 116 | * 0.2 (2012-09-07) 117 | * Ratchet passes every non-binary-frame test from the Autobahn Testsuite 118 | * Major performance improvements 119 | * BC: Renamed "WampServer" to "ServerProtocol" 120 | * BC: New "WampServer" component passes Topic container objects of subscribed Connections 121 | * Option to turn off UTF-8 checks in order to increase performance 122 | * Switched dependency guzzle/guzzle to guzzle/http (no API changes) 123 | * mbstring no longer required 124 | 125 | * 0.1.5 (2012-07-12) 126 | * BF: Error where service wouldn't run on PHP <= 5.3.8 127 | * Dependency library updates 128 | 129 | * 0.1.4 (2012-06-17) 130 | * Fixed dozens of failing AB tests 131 | * BF: Proper socket buffer handling 132 | 133 | * 0.1.3 (2012-06-15) 134 | * Major refactor inside WebSocket protocol handling, more loosley coupled 135 | * BF: Proper error handling on failed WebSocket connections 136 | * BF: Handle TCP message concatenation 137 | * Inclusion of the AutobahnTestSuite checking WebSocket protocol compliance 138 | * mb_string now a requirement 139 | 140 | * 0.1.2 (2012-05-19) 141 | * BC/BF: Updated WAMP API to coincide with the official spec 142 | * Tweaks to improve running as a long lived process 143 | 144 | * 0.1.1 (2012-05-14) 145 | * Separated interfaces allowing WebSockets to support multiple sub protocols 146 | * BF: remoteAddress variable on connections returns proper value 147 | 148 | * 0.1 (2012-05-11) 149 | * First release with components: IoServer, WsServer, SessionProvider, WampServer, FlashPolicy, IpBlackList 150 | * I/O now handled by React, making Ratchet fully asynchronous 151 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Chris Boden 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # This file is intended to ease the author's development and testing process 2 | # Users do not need to use `make`; Ratchet does not need to be compiled 3 | 4 | test: 5 | vendor/bin/phpunit 6 | 7 | cover: 8 | vendor/bin/phpunit --coverage-text --coverage-html=reports/coverage 9 | 10 | abtests: 11 | ulimit -n 2048 && php tests/autobahn/bin/fuzzingserver.php 8001 LibEvent & 12 | ulimit -n 2048 && php tests/autobahn/bin/fuzzingserver.php 8002 StreamSelect & 13 | ulimit -n 2048 && php tests/autobahn/bin/fuzzingserver.php 8004 LibEv & 14 | wstest -m testeeserver -w ws://localhost:8000 & 15 | sleep 1 16 | wstest -m fuzzingclient -s tests/autobahn/fuzzingclient-all.json 17 | killall php wstest 18 | 19 | abtest: 20 | ulimit -n 2048 && php tests/autobahn/bin/fuzzingserver.php 8000 StreamSelect & 21 | sleep 1 22 | wstest -m fuzzingclient -s tests/autobahn/fuzzingclient-quick.json 23 | killall php 24 | 25 | profile: 26 | php -d 'xdebug.profiler_enable=1' tests/autobahn/bin/fuzzingserver.php 8000 LibEvent & 27 | sleep 1 28 | wstest -m fuzzingclient -s tests/autobahn/fuzzingclient-profile.json 29 | killall php 30 | 31 | apidocs: 32 | apigen --title Ratchet -d reports/api \ 33 | -s src/ \ 34 | -s vendor/ratchet/rfc6455/src \ 35 | -s vendor/react/event-loop/src \ 36 | -s vendor/react/socket/src \ 37 | -s vendor/react/stream/src \ 38 | -s vendor/psr/http-message/src \ 39 | -s vendor/symfony/http-foundation/Session \ 40 | -s vendor/symfony/routing \ 41 | -s vendor/evenement/evenement/src/Evenement \ 42 | --exclude=vendor/symfony/routing/Tests \ 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ratchet 2 | 3 | [![CI status](https://github.com/ratchetphp/Ratchet/actions/workflows/ci.yml/badge.svg)](https://github.com/ratchetphp/Ratchet/actions) 4 | [![Autobahn Testsuite](https://img.shields.io/badge/Autobahn-passing-brightgreen.svg)](http://socketo.me/reports/ab/index.html) 5 | [![Latest Stable Version](https://poser.pugx.org/cboden/ratchet/v/stable.png)](https://packagist.org/packages/cboden/ratchet) 6 | 7 | A PHP library for asynchronously serving WebSockets. 8 | Build up your application through simple interfaces and re-use your application without changing any of its code just by combining different components. 9 | 10 | ## Reviving Ratchet! 11 | 12 | We're currently aiming to revive Ratchet to get it up to date with the latest versions and use this as a starting point for bigger updates to come. 13 | We need your help to achieve this goal, see [ticket #1054](https://github.com/ratchetphp/Ratchet/issues/1054) for ways to help out. ❤️ 14 | 15 | ## Requirements 16 | 17 | Shell access is required and root access is recommended. 18 | To avoid proxy/firewall blockage it's recommended WebSockets are requested on port 80 or 443 (SSL), which requires root access. 19 | In order to do this, along with your sync web stack, you can either use a reverse proxy or two separate machines. 20 | You can find more details in the [server conf docs](http://socketo.me/docs/deploy#server_configuration). 21 | 22 | ### Documentation 23 | 24 | User and API documentation is available on Ratchet's website: http://socketo.me 25 | 26 | See https://github.com/cboden/Ratchet-examples for some out-of-the-box working demos using Ratchet. 27 | 28 | Need help? Have a question? Want to provide feedback? Write a message on the [Google Groups Mailing List](https://groups.google.com/forum/#!forum/ratchet-php). 29 | 30 | --- 31 | 32 | ### A quick example 33 | 34 | ```php 35 | clients = new \SplObjectStorage; 51 | } 52 | 53 | public function onOpen(ConnectionInterface $conn) { 54 | $this->clients->attach($conn); 55 | } 56 | 57 | public function onMessage(ConnectionInterface $from, $msg) { 58 | foreach ($this->clients as $client) { 59 | if ($from != $client) { 60 | $client->send($msg); 61 | } 62 | } 63 | } 64 | 65 | public function onClose(ConnectionInterface $conn) { 66 | $this->clients->detach($conn); 67 | } 68 | 69 | public function onError(ConnectionInterface $conn, \Exception $e) { 70 | $conn->close(); 71 | } 72 | } 73 | 74 | // Run the server application through the WebSocket protocol on port 8080 75 | $app = new Ratchet\App('localhost', 8080); 76 | $app->route('/chat', new MyChat, array('*')); 77 | $app->route('/echo', new Ratchet\Server\EchoServer, array('*')); 78 | $app->run(); 79 | ``` 80 | 81 | $ php chat.php 82 | 83 | ```javascript 84 | // Then some JavaScript in the browser: 85 | var conn = new WebSocket('ws://localhost:8080/echo'); 86 | conn.onmessage = function(e) { console.log(e.data); }; 87 | conn.onopen = function(e) { conn.send('Hello Me!'); }; 88 | ``` 89 | 90 | ## Install 91 | 92 | The recommended way to install this library is [through Composer](https://getcomposer.org/). 93 | [New to Composer?](https://getcomposer.org/doc/00-intro.md) 94 | 95 | This will install the latest supported version: 96 | 97 | ```bash 98 | composer require cboden/ratchet:^0.4.4 99 | ``` 100 | 101 | See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. 102 | 103 | This project aims to run on any platform and thus does not require any PHP 104 | extensions and supports running on legacy PHP 5.4 through current PHP 8+. 105 | It's *highly recommended to use the latest supported PHP version* for this project. 106 | 107 | See above note about [Reviving Ratchet](#reviving-ratchet) for newer PHP support. 108 | 109 | ## License 110 | 111 | MIT, see [LICENSE file](LICENSE). 112 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | Please report security issues to: 6 | 7 | * Chris Boden [cboden@gmail.com](cboden@gmail.com) 8 | * Matt Bonneau [matt@bonneau.net](matt@bonneau.net) 9 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cboden/ratchet" 3 | , "type": "library" 4 | , "description": "PHP WebSocket library" 5 | , "keywords": ["WebSockets", "Server", "Ratchet", "Sockets", "WebSocket"] 6 | , "homepage": "http://socketo.me" 7 | , "license": "MIT" 8 | , "authors": [ 9 | { 10 | "name": "Chris Boden" 11 | , "email": "cboden@gmail.com" 12 | , "role": "Developer" 13 | } 14 | , { 15 | "name": "Matt Bonneau" 16 | , "role": "Developer" 17 | } 18 | ] 19 | , "support": { 20 | "issues": "https://github.com/ratchetphp/Ratchet/issues" 21 | , "chat": "https://gitter.im/reactphp/reactphp" 22 | } 23 | , "autoload": { 24 | "psr-4": { 25 | "Ratchet\\": "src/Ratchet" 26 | } 27 | } 28 | , "require": { 29 | "php": ">=5.4.2" 30 | , "ratchet/rfc6455": "^0.3.1" 31 | , "react/socket": "^1.0 || ^0.8 || ^0.7 || ^0.6 || ^0.5" 32 | , "react/event-loop": "^1.0 || ^0.5 || ^0.4" 33 | , "guzzlehttp/psr7": "^1.7|^2.0" 34 | , "symfony/http-foundation": "^2.6|^3.0|^4.0|^5.0|^6.0|^7.0" 35 | , "symfony/routing": "^2.6|^3.0|^4.0|^5.0|^6.0|^7.0" 36 | } 37 | , "require-dev": { 38 | "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | ./tests/unit/ 13 | 14 | 15 | 16 | 17 | ./src/ 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /phpunit.xml.legacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | ./tests/unit/ 11 | 12 | 13 | 14 | 15 | ./src/ 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/Ratchet/AbstractConnectionDecorator.php: -------------------------------------------------------------------------------- 1 | wrappedConn = $conn; 22 | } 23 | 24 | /** 25 | * @return ConnectionInterface 26 | */ 27 | protected function getConnection() { 28 | return $this->wrappedConn; 29 | } 30 | 31 | public function __set($name, $value) { 32 | $this->wrappedConn->$name = $value; 33 | } 34 | 35 | public function __get($name) { 36 | return $this->wrappedConn->$name; 37 | } 38 | 39 | public function __isset($name) { 40 | return isset($this->wrappedConn->$name); 41 | } 42 | 43 | public function __unset($name) { 44 | unset($this->wrappedConn->$name); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Ratchet/App.php: -------------------------------------------------------------------------------- 1 | httpHost = $httpHost; 81 | $this->port = $port; 82 | 83 | // prefer SocketServer (reactphp/socket v1.9+) over legacy \React\Socket\Server 84 | $socket = class_exists('React\Socket\SocketServer') ? new SocketServer($address . ':' . $port, $context, $loop) : new LegacySocketServer($address . ':' . $port, $loop, $context); 85 | 86 | $this->routes = new RouteCollection; 87 | $this->_server = new IoServer(new HttpServer(new Router(new UrlMatcher($this->routes, new RequestContext))), $socket, $loop); 88 | 89 | $policy = new FlashPolicy; 90 | $policy->addAllowedAccess($httpHost, 80); 91 | $policy->addAllowedAccess($httpHost, $port); 92 | 93 | if (80 == $port) { 94 | $flashUri = '0.0.0.0:843'; 95 | } else { 96 | $flashUri = 8843; 97 | } 98 | 99 | // prefer SocketServer (reactphp/socket v1.9+) over legacy \React\Socket\Server 100 | $flashSock = class_exists('React\Socket\SocketServer') ? new SocketServer($flashUri, [], $loop) : new LegacySocketServer($flashUri, $loop); 101 | $this->flashServer = new IoServer($policy, $flashSock); 102 | } 103 | 104 | /** 105 | * Add an endpoint/application to the server 106 | * @param string $path The URI the client will connect to 107 | * @param ComponentInterface $controller Your application to server for the route. If not specified, assumed to be for a WebSocket 108 | * @param array $allowedOrigins An array of hosts allowed to connect (same host by default), ['*'] for any 109 | * @param string $httpHost Override the $httpHost variable provided in the __construct 110 | * @return ComponentInterface|WsServer 111 | */ 112 | public function route($path, ComponentInterface $controller, array $allowedOrigins = array(), $httpHost = null) { 113 | if ($controller instanceof HttpServerInterface || $controller instanceof WsServer) { 114 | $decorated = $controller; 115 | } elseif ($controller instanceof WampServerInterface) { 116 | $decorated = new WsServer(new WampServer($controller)); 117 | $decorated->enableKeepAlive($this->_server->loop); 118 | } elseif ($controller instanceof MessageComponentInterface || $controller instanceof WsMessageComponentInterface) { 119 | $decorated = new WsServer($controller); 120 | $decorated->enableKeepAlive($this->_server->loop); 121 | } else { 122 | $decorated = $controller; 123 | } 124 | 125 | if ($httpHost === null) { 126 | $httpHost = $this->httpHost; 127 | } 128 | 129 | $allowedOrigins = array_values($allowedOrigins); 130 | if (0 === count($allowedOrigins)) { 131 | $allowedOrigins[] = $httpHost; 132 | } 133 | if ('*' !== $allowedOrigins[0]) { 134 | $decorated = new OriginCheck($decorated, $allowedOrigins); 135 | } 136 | 137 | //allow origins in flash policy server 138 | if(empty($this->flashServer) === false) { 139 | foreach($allowedOrigins as $allowedOrgin) { 140 | $this->flashServer->app->addAllowedAccess($allowedOrgin, $this->port); 141 | } 142 | } 143 | 144 | $this->routes->add('rr-' . ++$this->_routeCounter, new Route($path, array('_controller' => $decorated), array('Origin' => $this->httpHost), array(), $httpHost, array(), array('GET'))); 145 | 146 | return $decorated; 147 | } 148 | 149 | /** 150 | * Run the server by entering the event loop 151 | */ 152 | public function run() { 153 | $this->_server->run(); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/Ratchet/ComponentInterface.php: -------------------------------------------------------------------------------- 1 | \Ratchet\VERSION 17 | ], $additional_headers)); 18 | 19 | $conn->send(Message::toString($response)); 20 | $conn->close(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Ratchet/Http/HttpRequestParser.php: -------------------------------------------------------------------------------- 1 | httpBuffer)) { 30 | $context->httpBuffer = ''; 31 | } 32 | 33 | $context->httpBuffer .= $data; 34 | 35 | if (strlen($context->httpBuffer) > (int)$this->maxSize) { 36 | throw new \OverflowException("Maximum buffer size of {$this->maxSize} exceeded parsing HTTP header"); 37 | } 38 | 39 | if ($this->isEom($context->httpBuffer)) { 40 | $request = $this->parse($context->httpBuffer); 41 | 42 | unset($context->httpBuffer); 43 | 44 | return $request; 45 | } 46 | } 47 | 48 | /** 49 | * Determine if the message has been buffered as per the HTTP specification 50 | * @param string $message 51 | * @return boolean 52 | */ 53 | public function isEom($message) { 54 | return (boolean)strpos($message, static::EOM); 55 | } 56 | 57 | /** 58 | * @param string $headers 59 | * @return \Psr\Http\Message\RequestInterface 60 | */ 61 | public function parse($headers) { 62 | return Message::parseRequest($headers); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Ratchet/Http/HttpServer.php: -------------------------------------------------------------------------------- 1 | _httpServer = $component; 26 | $this->_reqParser = new HttpRequestParser; 27 | } 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | public function onOpen(ConnectionInterface $conn) { 33 | $conn->httpHeadersReceived = false; 34 | } 35 | 36 | /** 37 | * {@inheritdoc} 38 | */ 39 | public function onMessage(ConnectionInterface $from, $msg) { 40 | if (true !== $from->httpHeadersReceived) { 41 | try { 42 | if (null === ($request = $this->_reqParser->onMessage($from, $msg))) { 43 | return; 44 | } 45 | } catch (\OverflowException $oe) { 46 | return $this->close($from, 413); 47 | } 48 | 49 | $from->httpHeadersReceived = true; 50 | 51 | return $this->_httpServer->onOpen($from, $request); 52 | } 53 | 54 | $this->_httpServer->onMessage($from, $msg); 55 | } 56 | 57 | /** 58 | * {@inheritdoc} 59 | */ 60 | public function onClose(ConnectionInterface $conn) { 61 | if ($conn->httpHeadersReceived) { 62 | $this->_httpServer->onClose($conn); 63 | } 64 | } 65 | 66 | /** 67 | * {@inheritdoc} 68 | */ 69 | public function onError(ConnectionInterface $conn, \Exception $e) { 70 | if ($conn->httpHeadersReceived) { 71 | $this->_httpServer->onError($conn, $e); 72 | } else { 73 | $this->close($conn, 500); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Ratchet/Http/HttpServerInterface.php: -------------------------------------------------------------------------------- 1 | _component = $component; 28 | $this->allowedOrigins += $allowed; 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | #[HackSupportForPHP8] public function onOpen(ConnectionInterface $conn, ?RequestInterface $request = null) { /* 35 | public function onOpen(ConnectionInterface $conn, RequestInterface $request = null) { /**/ 36 | $header = (string)$request->getHeader('Origin')[0]; 37 | $origin = parse_url($header, PHP_URL_HOST) ?: $header; 38 | 39 | if (!in_array($origin, $this->allowedOrigins)) { 40 | return $this->close($conn, 403); 41 | } 42 | 43 | return $this->_component->onOpen($conn, $request); 44 | } 45 | 46 | /** 47 | * {@inheritdoc} 48 | */ 49 | function onMessage(ConnectionInterface $from, $msg) { 50 | return $this->_component->onMessage($from, $msg); 51 | } 52 | 53 | /** 54 | * {@inheritdoc} 55 | */ 56 | function onClose(ConnectionInterface $conn) { 57 | return $this->_component->onClose($conn); 58 | } 59 | 60 | /** 61 | * {@inheritdoc} 62 | */ 63 | function onError(ConnectionInterface $conn, \Exception $e) { 64 | return $this->_component->onError($conn, $e); 65 | } 66 | } -------------------------------------------------------------------------------- /src/Ratchet/Http/Router.php: -------------------------------------------------------------------------------- 1 | _matcher = $matcher; 22 | $this->_noopController = new NoOpHttpServerController; 23 | } 24 | 25 | /** 26 | * {@inheritdoc} 27 | * @throws \UnexpectedValueException If a controller is not \Ratchet\Http\HttpServerInterface 28 | */ 29 | #[HackSupportForPHP8] public function onOpen(ConnectionInterface $conn, ?RequestInterface $request = null) { /* 30 | public function onOpen(ConnectionInterface $conn, RequestInterface $request = null) { /**/ 31 | if (null === $request) { 32 | throw new \UnexpectedValueException('$request can not be null'); 33 | } 34 | 35 | $conn->controller = $this->_noopController; 36 | 37 | $uri = $request->getUri(); 38 | 39 | $context = $this->_matcher->getContext(); 40 | $context->setMethod($request->getMethod()); 41 | $context->setHost($uri->getHost()); 42 | 43 | try { 44 | $route = $this->_matcher->match($uri->getPath()); 45 | } catch (MethodNotAllowedException $nae) { 46 | return $this->close($conn, 405, array('Allow' => $nae->getAllowedMethods())); 47 | } catch (ResourceNotFoundException $nfe) { 48 | return $this->close($conn, 404); 49 | } 50 | 51 | if (is_string($route['_controller']) && class_exists($route['_controller'])) { 52 | $route['_controller'] = new $route['_controller']; 53 | } 54 | 55 | if (!($route['_controller'] instanceof HttpServerInterface)) { 56 | throw new \UnexpectedValueException('All routes must implement Ratchet\Http\HttpServerInterface'); 57 | } 58 | 59 | $parameters = []; 60 | foreach($route as $key => $value) { 61 | if ((is_string($key)) && ('_' !== substr($key, 0, 1))) { 62 | $parameters[$key] = $value; 63 | } 64 | } 65 | $parameters = array_merge($parameters, Query::parse($uri->getQuery() ?: '')); 66 | 67 | $request = $request->withUri($uri->withQuery(Query::build($parameters))); 68 | 69 | $conn->controller = $route['_controller']; 70 | $conn->controller->onOpen($conn, $request); 71 | } 72 | 73 | /** 74 | * {@inheritdoc} 75 | */ 76 | public function onMessage(ConnectionInterface $from, $msg) { 77 | $from->controller->onMessage($from, $msg); 78 | } 79 | 80 | /** 81 | * {@inheritdoc} 82 | */ 83 | public function onClose(ConnectionInterface $conn) { 84 | if (isset($conn->controller)) { 85 | $conn->controller->onClose($conn); 86 | } 87 | } 88 | 89 | /** 90 | * {@inheritdoc} 91 | */ 92 | public function onError(ConnectionInterface $conn, \Exception $e) { 93 | if (isset($conn->controller)) { 94 | $conn->controller->onError($conn, $e); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Ratchet/MessageComponentInterface.php: -------------------------------------------------------------------------------- 1 | send($msg); 15 | } 16 | 17 | public function onClose(ConnectionInterface $conn) { 18 | } 19 | 20 | public function onError(ConnectionInterface $conn, \Exception $e) { 21 | $conn->close(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Ratchet/Server/FlashPolicy.php: -------------------------------------------------------------------------------- 1 | '; 23 | 24 | /** 25 | * Stores an array of allowed domains and their ports 26 | * @var array 27 | */ 28 | protected $_access = array(); 29 | 30 | /** 31 | * @var string 32 | */ 33 | protected $_siteControl = ''; 34 | 35 | /** 36 | * @var string 37 | */ 38 | protected $_cache = ''; 39 | 40 | /** 41 | * @var string 42 | */ 43 | protected $_cacheValid = false; 44 | 45 | /** 46 | * Add a domain to an allowed access list. 47 | * 48 | * @param string $domain Specifies a requesting domain to be granted access. Both named domains and IP 49 | * addresses are acceptable values. Subdomains are considered different domains. A wildcard (*) can 50 | * be used to match all domains when used alone, or multiple domains (subdomains) when used as a 51 | * prefix for an explicit, second-level domain name separated with a dot (.) 52 | * @param string $ports A comma-separated list of ports or range of ports that a socket connection 53 | * is allowed to connect to. A range of ports is specified through a dash (-) between two port numbers. 54 | * Ranges can be used with individual ports when separated with a comma. A single wildcard (*) can 55 | * be used to allow all ports. 56 | * @param bool $secure 57 | * @throws \UnexpectedValueException 58 | * @return FlashPolicy 59 | */ 60 | public function addAllowedAccess($domain, $ports = '*', $secure = false) { 61 | if (!$this->validateDomain($domain)) { 62 | throw new \UnexpectedValueException('Invalid domain'); 63 | } 64 | 65 | if (!$this->validatePorts($ports)) { 66 | throw new \UnexpectedValueException('Invalid Port'); 67 | } 68 | 69 | $this->_access[] = array($domain, $ports, (boolean)$secure); 70 | $this->_cacheValid = false; 71 | 72 | return $this; 73 | } 74 | 75 | /** 76 | * Removes all domains from the allowed access list. 77 | * 78 | * @return \Ratchet\Server\FlashPolicy 79 | */ 80 | public function clearAllowedAccess() { 81 | $this->_access = array(); 82 | $this->_cacheValid = false; 83 | 84 | return $this; 85 | } 86 | 87 | /** 88 | * site-control defines the meta-policy for the current domain. A meta-policy specifies acceptable 89 | * domain policy files other than the master policy file located in the target domain's root and named 90 | * crossdomain.xml. 91 | * 92 | * @param string $permittedCrossDomainPolicies 93 | * @throws \UnexpectedValueException 94 | * @return FlashPolicy 95 | */ 96 | public function setSiteControl($permittedCrossDomainPolicies = 'all') { 97 | if (!$this->validateSiteControl($permittedCrossDomainPolicies)) { 98 | throw new \UnexpectedValueException('Invalid site control set'); 99 | } 100 | 101 | $this->_siteControl = $permittedCrossDomainPolicies; 102 | $this->_cacheValid = false; 103 | 104 | return $this; 105 | } 106 | 107 | /** 108 | * {@inheritdoc} 109 | */ 110 | public function onOpen(ConnectionInterface $conn) { 111 | } 112 | 113 | /** 114 | * {@inheritdoc} 115 | */ 116 | public function onMessage(ConnectionInterface $from, $msg) { 117 | if (!$this->_cacheValid) { 118 | $this->_cache = $this->renderPolicy()->asXML(); 119 | $this->_cacheValid = true; 120 | } 121 | 122 | $from->send($this->_cache . "\0"); 123 | $from->close(); 124 | } 125 | 126 | /** 127 | * {@inheritdoc} 128 | */ 129 | public function onClose(ConnectionInterface $conn) { 130 | } 131 | 132 | /** 133 | * {@inheritdoc} 134 | */ 135 | public function onError(ConnectionInterface $conn, \Exception $e) { 136 | $conn->close(); 137 | } 138 | 139 | /** 140 | * Builds the crossdomain file based on the template policy 141 | * 142 | * @throws \UnexpectedValueException 143 | * @return \SimpleXMLElement 144 | */ 145 | public function renderPolicy() { 146 | $policy = new \SimpleXMLElement($this->_policy); 147 | 148 | $siteControl = $policy->addChild('site-control'); 149 | 150 | if ($this->_siteControl == '') { 151 | $this->setSiteControl(); 152 | } 153 | 154 | $siteControl->addAttribute('permitted-cross-domain-policies', $this->_siteControl); 155 | 156 | if (empty($this->_access)) { 157 | throw new \UnexpectedValueException('You must add a domain through addAllowedAccess()'); 158 | } 159 | 160 | foreach ($this->_access as $access) { 161 | $tmp = $policy->addChild('allow-access-from'); 162 | $tmp->addAttribute('domain', $access[0]); 163 | $tmp->addAttribute('to-ports', $access[1]); 164 | $tmp->addAttribute('secure', ($access[2] === true) ? 'true' : 'false'); 165 | } 166 | 167 | return $policy; 168 | } 169 | 170 | /** 171 | * Make sure the proper site control was passed 172 | * 173 | * @param string $permittedCrossDomainPolicies 174 | * @return bool 175 | */ 176 | public function validateSiteControl($permittedCrossDomainPolicies) { 177 | //'by-content-type' and 'by-ftp-filename' are not available for sockets 178 | return (bool)in_array($permittedCrossDomainPolicies, array('none', 'master-only', 'all')); 179 | } 180 | 181 | /** 182 | * Validate for proper domains (wildcards allowed) 183 | * 184 | * @param string $domain 185 | * @return bool 186 | */ 187 | public function validateDomain($domain) { 188 | return (bool)preg_match("/^((http(s)?:\/\/)?([a-z0-9-_]+\.|\*\.)*([a-z0-9-_\.]+)|\*)$/i", $domain); 189 | } 190 | 191 | /** 192 | * Make sure valid ports were passed 193 | * 194 | * @param string $port 195 | * @return bool 196 | */ 197 | public function validatePorts($port) { 198 | return (bool)preg_match('/^(\*|(\d+[,-]?)*\d+)$/', $port); 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/Ratchet/Server/IoConnection.php: -------------------------------------------------------------------------------- 1 | conn = $conn; 22 | } 23 | 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | public function send($data) { 28 | $this->conn->write($data); 29 | 30 | return $this; 31 | } 32 | 33 | /** 34 | * {@inheritdoc} 35 | */ 36 | public function close() { 37 | $this->conn->end(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Ratchet/Server/IoServer.php: -------------------------------------------------------------------------------- 1 | loop = $loop; 51 | $this->app = $app; 52 | $this->socket = $socket; 53 | 54 | $socket->on('connection', array($this, 'handleConnect')); 55 | } 56 | 57 | /** 58 | * @param \Ratchet\MessageComponentInterface $component The application that I/O will call when events are received 59 | * @param int $port The port to server sockets on 60 | * @param string $address The address to receive sockets on (0.0.0.0 means receive connections from any) 61 | * @return IoServer 62 | */ 63 | public static function factory(MessageComponentInterface $component, $port = 80, $address = '0.0.0.0') { 64 | // prefer default Loop (reactphp/event-loop v1.2+) over legacy \React\EventLoop\Factory 65 | $loop = class_exists('React\EventLoop\Loop') ? Loop::get() : LegacyLoopFactory::create(); 66 | 67 | // prefer SocketServer (reactphp/socket v1.9+) over legacy \React\Socket\Server 68 | $socket = class_exists('React\Socket\SocketServer') ? new SocketServer($address . ':' . $port, [], $loop) : new LegacySocketServer($address . ':' . $port, $loop); 69 | 70 | return new static($component, $socket, $loop); 71 | } 72 | 73 | /** 74 | * Run the application by entering the event loop 75 | * @throws \RuntimeException If a loop was not previously specified 76 | */ 77 | public function run() { 78 | if (null === $this->loop) { 79 | throw new \RuntimeException("A React Loop was not provided during instantiation"); 80 | } 81 | 82 | // @codeCoverageIgnoreStart 83 | $this->loop->run(); 84 | // @codeCoverageIgnoreEnd 85 | } 86 | 87 | /** 88 | * Triggered when a new connection is received from React 89 | * @param \React\Socket\ConnectionInterface $conn 90 | */ 91 | public function handleConnect(SocketConnection $conn) { 92 | // assign dynamic `$decor` property used by Ratchet without raising notice on PHP 8.2+ 93 | // need this hack because we can't use `#[\AllowDynamicProperties]` on vendor code 94 | set_error_handler(function () { }, E_DEPRECATED); 95 | $conn->decor = new IoConnection($conn); 96 | restore_error_handler(); 97 | 98 | $conn->decor->resourceId = (int)$conn->stream; 99 | 100 | $uri = $conn->getRemoteAddress(); 101 | $conn->decor->remoteAddress = trim( 102 | parse_url((strpos($uri, '://') === false ? 'tcp://' : '') . $uri, PHP_URL_HOST), 103 | '[]' 104 | ); 105 | 106 | $this->app->onOpen($conn->decor); 107 | 108 | $conn->on('data', function ($data) use ($conn) { 109 | $this->handleData($data, $conn); 110 | }); 111 | $conn->on('close', function () use ($conn) { 112 | $this->handleEnd($conn); 113 | }); 114 | $conn->on('error', function (\Exception $e) use ($conn) { 115 | $this->handleError($e, $conn); 116 | }); 117 | } 118 | 119 | /** 120 | * Data has been received from React 121 | * @param string $data 122 | * @param \React\Socket\ConnectionInterface $conn 123 | */ 124 | public function handleData($data, $conn) { 125 | try { 126 | $this->app->onMessage($conn->decor, $data); 127 | } catch (\Exception $e) { 128 | $this->handleError($e, $conn); 129 | } 130 | } 131 | 132 | /** 133 | * A connection has been closed by React 134 | * @param \React\Socket\ConnectionInterface $conn 135 | */ 136 | public function handleEnd($conn) { 137 | try { 138 | $this->app->onClose($conn->decor); 139 | } catch (\Exception $e) { 140 | $this->handleError($e, $conn); 141 | } 142 | 143 | unset($conn->decor); 144 | } 145 | 146 | /** 147 | * An error has occurred, let the listening application know 148 | * @param \Exception $e 149 | * @param \React\Socket\ConnectionInterface $conn 150 | */ 151 | public function handleError(\Exception $e, $conn) { 152 | $this->app->onError($conn->decor, $e); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/Ratchet/Server/IpBlackList.php: -------------------------------------------------------------------------------- 1 | _decorating = $component; 22 | } 23 | 24 | /** 25 | * Add an address to the blacklist that will not be allowed to connect to your application 26 | * @param string $ip IP address to block from connecting to your application 27 | * @return IpBlackList 28 | */ 29 | public function blockAddress($ip) { 30 | $this->_blacklist[$ip] = true; 31 | 32 | return $this; 33 | } 34 | 35 | /** 36 | * Unblock an address so they can access your application again 37 | * @param string $ip IP address to unblock from connecting to your application 38 | * @return IpBlackList 39 | */ 40 | public function unblockAddress($ip) { 41 | if (isset($this->_blacklist[$this->filterAddress($ip)])) { 42 | unset($this->_blacklist[$this->filterAddress($ip)]); 43 | } 44 | 45 | return $this; 46 | } 47 | 48 | /** 49 | * @param string $address 50 | * @return bool 51 | */ 52 | public function isBlocked($address) { 53 | return (isset($this->_blacklist[$this->filterAddress($address)])); 54 | } 55 | 56 | /** 57 | * Get an array of all the addresses blocked 58 | * @return array 59 | */ 60 | public function getBlockedAddresses() { 61 | return array_keys($this->_blacklist); 62 | } 63 | 64 | /** 65 | * @param string $address 66 | * @return string 67 | */ 68 | public function filterAddress($address) { 69 | if (strstr($address, ':') && substr_count($address, '.') == 3) { 70 | list($address, $port) = explode(':', $address); 71 | } 72 | 73 | return $address; 74 | } 75 | 76 | /** 77 | * {@inheritdoc} 78 | */ 79 | function onOpen(ConnectionInterface $conn) { 80 | if ($this->isBlocked($conn->remoteAddress)) { 81 | return $conn->close(); 82 | } 83 | 84 | return $this->_decorating->onOpen($conn); 85 | } 86 | 87 | /** 88 | * {@inheritdoc} 89 | */ 90 | function onMessage(ConnectionInterface $from, $msg) { 91 | return $this->_decorating->onMessage($from, $msg); 92 | } 93 | 94 | /** 95 | * {@inheritdoc} 96 | */ 97 | function onClose(ConnectionInterface $conn) { 98 | if (!$this->isBlocked($conn->remoteAddress)) { 99 | $this->_decorating->onClose($conn); 100 | } 101 | } 102 | 103 | /** 104 | * {@inheritdoc} 105 | */ 106 | function onError(ConnectionInterface $conn, \Exception $e) { 107 | if (!$this->isBlocked($conn->remoteAddress)) { 108 | $this->_decorating->onError($conn, $e); 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Ratchet/Session/Serialize/HandlerInterface.php: -------------------------------------------------------------------------------- 1 | $bucketData) { 15 | $preSerialized[] = $bucket . '|' . serialize($bucketData); 16 | } 17 | $serialized = implode('', $preSerialized); 18 | } 19 | 20 | return $serialized; 21 | } 22 | 23 | /** 24 | * {@inheritdoc} 25 | * @link http://ca2.php.net/manual/en/function.session-decode.php#108037 Code from this comment on php.net 26 | * @throws \UnexpectedValueException If there is a problem parsing the data 27 | */ 28 | public function unserialize($raw) { 29 | $returnData = array(); 30 | $offset = 0; 31 | 32 | while ($offset < strlen($raw)) { 33 | if (!strstr(substr($raw, $offset), "|")) { 34 | throw new \UnexpectedValueException("invalid data, remaining: " . substr($raw, $offset)); 35 | } 36 | 37 | $pos = strpos($raw, "|", $offset); 38 | $num = $pos - $offset; 39 | $varname = substr($raw, $offset, $num); 40 | $offset += $num + 1; 41 | 42 | // try to unserialize one piece of data from current offset, ignoring any warnings for trailing data on PHP 8.3+ 43 | // @link https://wiki.php.net/rfc/unserialize_warn_on_trailing_data 44 | $data = @unserialize(substr($raw, $offset)); 45 | 46 | $returnData[$varname] = $data; 47 | $offset += strlen(serialize($data)); 48 | } 49 | 50 | return $returnData; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Ratchet/Session/SessionProvider.php: -------------------------------------------------------------------------------- 1 | _app = $app; 52 | $this->_handler = $handler; 53 | $this->_null = new NullSessionHandler; 54 | 55 | ini_set('session.auto_start', 0); 56 | ini_set('session.cache_limiter', ''); 57 | ini_set('session.use_cookies', 0); 58 | 59 | $this->setOptions($options); 60 | 61 | if (null === $serializer) { 62 | $serialClass = __NAMESPACE__ . "\\Serialize\\{$this->toClassCase(ini_get('session.serialize_handler'))}Handler"; // awesome/terrible hack, eh? 63 | if (!class_exists($serialClass)) { 64 | throw new \RuntimeException('Unable to parse session serialize handler'); 65 | } 66 | 67 | $serializer = new $serialClass; 68 | } 69 | 70 | $this->_serializer = $serializer; 71 | } 72 | 73 | /** 74 | * {@inheritdoc} 75 | */ 76 | #[HackSupportForPHP8] public function onOpen(ConnectionInterface $conn, ?RequestInterface $request = null) { /* 77 | public function onOpen(ConnectionInterface $conn, RequestInterface $request = null) { /**/ 78 | $sessionName = ini_get('session.name'); 79 | 80 | $id = array_reduce($request->getHeader('Cookie'), function($accumulator, $cookie) use ($sessionName) { 81 | if ($accumulator) { 82 | return $accumulator; 83 | } 84 | 85 | $crumbs = $this->parseCookie($cookie); 86 | 87 | return isset($crumbs['cookies'][$sessionName]) ? $crumbs['cookies'][$sessionName] : false; 88 | }, false); 89 | 90 | if (null === $request || false === $id) { 91 | $saveHandler = $this->_null; 92 | $id = ''; 93 | } else { 94 | $saveHandler = $this->_handler; 95 | } 96 | 97 | $conn->Session = new Session(new VirtualSessionStorage($saveHandler, $id, $this->_serializer)); 98 | 99 | if (ini_get('session.auto_start')) { 100 | $conn->Session->start(); 101 | } 102 | 103 | return $this->_app->onOpen($conn, $request); 104 | } 105 | 106 | /** 107 | * {@inheritdoc} 108 | */ 109 | function onMessage(ConnectionInterface $from, $msg) { 110 | return $this->_app->onMessage($from, $msg); 111 | } 112 | 113 | /** 114 | * {@inheritdoc} 115 | */ 116 | function onClose(ConnectionInterface $conn) { 117 | // "close" session for Connection 118 | 119 | return $this->_app->onClose($conn); 120 | } 121 | 122 | /** 123 | * {@inheritdoc} 124 | */ 125 | function onError(ConnectionInterface $conn, \Exception $e) { 126 | return $this->_app->onError($conn, $e); 127 | } 128 | 129 | /** 130 | * Set all the php session. ini options 131 | * © Symfony 132 | * @param array $options 133 | * @return array 134 | */ 135 | protected function setOptions(array $options) { 136 | $all = array( 137 | 'auto_start', 'cache_limiter', 'cookie_domain', 'cookie_httponly', 138 | 'cookie_lifetime', 'cookie_path', 'cookie_secure', 139 | 'entropy_file', 'entropy_length', 'gc_divisor', 140 | 'gc_maxlifetime', 'gc_probability', 'hash_bits_per_character', 141 | 'hash_function', 'name', 'referer_check', 142 | 'serialize_handler', 'use_cookies', 143 | 'use_only_cookies', 'use_trans_sid', 'upload_progress.enabled', 144 | 'upload_progress.cleanup', 'upload_progress.prefix', 'upload_progress.name', 145 | 'upload_progress.freq', 'upload_progress.min-freq', 'url_rewriter.tags' 146 | ); 147 | 148 | foreach ($all as $key) { 149 | if (!array_key_exists($key, $options)) { 150 | $options[$key] = ini_get("session.{$key}"); 151 | } else { 152 | ini_set("session.{$key}", $options[$key]); 153 | } 154 | } 155 | 156 | return $options; 157 | } 158 | 159 | /** 160 | * @param string $langDef Input to convert 161 | * @return string 162 | */ 163 | protected function toClassCase($langDef) { 164 | return str_replace(' ', '', ucwords(str_replace('_', ' ', $langDef))); 165 | } 166 | 167 | /** 168 | * Taken from Guzzle3 169 | */ 170 | private static $cookieParts = array( 171 | 'domain' => 'Domain', 172 | 'path' => 'Path', 173 | 'max_age' => 'Max-Age', 174 | 'expires' => 'Expires', 175 | 'version' => 'Version', 176 | 'secure' => 'Secure', 177 | 'port' => 'Port', 178 | 'discard' => 'Discard', 179 | 'comment' => 'Comment', 180 | 'comment_url' => 'Comment-Url', 181 | 'http_only' => 'HttpOnly' 182 | ); 183 | 184 | /** 185 | * Taken from Guzzle3 186 | */ 187 | private function parseCookie($cookie, $host = null, $path = null, $decode = false) { 188 | // Explode the cookie string using a series of semicolons 189 | $pieces = array_filter(array_map('trim', explode(';', $cookie))); 190 | 191 | // The name of the cookie (first kvp) must include an equal sign. 192 | if (empty($pieces) || !strpos($pieces[0], '=')) { 193 | return false; 194 | } 195 | 196 | // Create the default return array 197 | $data = array_merge(array_fill_keys(array_keys(self::$cookieParts), null), array( 198 | 'cookies' => array(), 199 | 'data' => array(), 200 | 'path' => $path ?: '/', 201 | 'http_only' => false, 202 | 'discard' => false, 203 | 'domain' => $host 204 | )); 205 | $foundNonCookies = 0; 206 | 207 | // Add the cookie pieces into the parsed data array 208 | foreach ($pieces as $part) { 209 | 210 | $cookieParts = explode('=', $part, 2); 211 | $key = trim($cookieParts[0]); 212 | 213 | if (count($cookieParts) == 1) { 214 | // Can be a single value (e.g. secure, httpOnly) 215 | $value = true; 216 | } else { 217 | // Be sure to strip wrapping quotes 218 | $value = trim($cookieParts[1], " \n\r\t\0\x0B\""); 219 | if ($decode) { 220 | $value = urldecode($value); 221 | } 222 | } 223 | 224 | // Only check for non-cookies when cookies have been found 225 | if (!empty($data['cookies'])) { 226 | foreach (self::$cookieParts as $mapValue => $search) { 227 | if (!strcasecmp($search, $key)) { 228 | $data[$mapValue] = $mapValue == 'port' ? array_map('trim', explode(',', $value)) : $value; 229 | $foundNonCookies++; 230 | continue 2; 231 | } 232 | } 233 | } 234 | 235 | // If cookies have not yet been retrieved, or this value was not found in the pieces array, treat it as a 236 | // cookie. IF non-cookies have been parsed, then this isn't a cookie, it's cookie data. Cookies then data. 237 | $data[$foundNonCookies ? 'data' : 'cookies'][$key] = $value; 238 | } 239 | 240 | // Calculate the expires date 241 | if (!$data['expires'] && $data['max_age']) { 242 | $data['expires'] = time() + (int) $data['max_age']; 243 | } 244 | 245 | return $data; 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /src/Ratchet/Session/Storage/Proxy/VirtualProxy.php: -------------------------------------------------------------------------------- 1 | 80200 && (new \ReflectionMethod('Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy','setId'))->hasReturnType()) { 6 | // alias to class for Symfony 7 on PHP 8.2+ using native types like `setId(string $id): void` 7 | class_alias(__NAMESPACE__ . '\\VirtualProxyForSymfony7', __NAMESPACE__ . '\\VirtualProxy'); 8 | } elseif (PHP_VERSION_ID > 80000 && (new \ReflectionMethod('Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy','getId'))->hasReturnType()) { 9 | // alias to class for Symfony 6 on PHP 8+ using native types like `getId(): string` 10 | class_alias(__NAMESPACE__ . '\\VirtualProxyForSymfony6', __NAMESPACE__ . '\\VirtualProxy'); 11 | } else { 12 | // fall back to class without native types 13 | 14 | class VirtualProxy extends SessionHandlerProxy { 15 | /** 16 | * @var string 17 | */ 18 | protected $_sessionId; 19 | 20 | /** 21 | * @var string 22 | */ 23 | protected $_sessionName; 24 | 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | public function __construct(\SessionHandlerInterface $handler) { 29 | parent::__construct($handler); 30 | 31 | $this->saveHandlerName = 'user'; 32 | $this->_sessionName = ini_get('session.name'); 33 | } 34 | 35 | /** 36 | * {@inheritdoc} 37 | */ 38 | public function getId() { 39 | return $this->_sessionId; 40 | } 41 | 42 | /** 43 | * {@inheritdoc} 44 | */ 45 | public function setId($id) { 46 | $this->_sessionId = $id; 47 | } 48 | 49 | /** 50 | * {@inheritdoc} 51 | */ 52 | public function getName() { 53 | return $this->_sessionName; 54 | } 55 | 56 | /** 57 | * DO NOT CALL THIS METHOD 58 | * @internal 59 | */ 60 | public function setName($name) { 61 | throw new \RuntimeException("Can not change session name in VirtualProxy"); 62 | } 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/Ratchet/Session/Storage/Proxy/VirtualProxyForSymfony6.php: -------------------------------------------------------------------------------- 1 | saveHandlerName = 'user'; 29 | $this->_sessionName = ini_get('session.name'); 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | public function getId(): string { 36 | return $this->_sessionId; 37 | } 38 | 39 | /** 40 | * {@inheritdoc} 41 | */ 42 | public function setId($id) { 43 | $this->_sessionId = $id; 44 | } 45 | 46 | /** 47 | * {@inheritdoc} 48 | */ 49 | public function getName(): string { 50 | return $this->_sessionName; 51 | } 52 | 53 | /** 54 | * DO NOT CALL THIS METHOD 55 | * @internal 56 | */ 57 | public function setName($name) { 58 | throw new \RuntimeException("Can not change session name in VirtualProxy"); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Ratchet/Session/Storage/Proxy/VirtualProxyForSymfony7.php: -------------------------------------------------------------------------------- 1 | saveHandlerName = 'user'; 29 | $this->_sessionName = ini_get('session.name'); 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | public function getId(): string { 36 | return $this->_sessionId; 37 | } 38 | 39 | /** 40 | * {@inheritdoc} 41 | */ 42 | public function setId(string $id): void { 43 | $this->_sessionId = $id; 44 | } 45 | 46 | /** 47 | * {@inheritdoc} 48 | */ 49 | public function getName(): string { 50 | return $this->_sessionName; 51 | } 52 | 53 | /** 54 | * DO NOT CALL THIS METHOD 55 | * @internal 56 | */ 57 | public function setName(string $name): void { 58 | throw new \RuntimeException("Can not change session name in VirtualProxy"); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Ratchet/Session/Storage/VirtualSessionStorage.php: -------------------------------------------------------------------------------- 1 | 80200 && (new \ReflectionMethod('Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage','save'))->hasReturnType()) { 8 | // alias to class for Symfony 7 on PHP 8.2+ using native types like `save(): void` 9 | class_alias(__NAMESPACE__ . '\\VirtualSessionStorageForSymfony7', __NAMESPACE__ . '\\VirtualSessionStorage'); 10 | } elseif (PHP_VERSION_ID > 80000 && (new \ReflectionMethod('Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage','start'))->hasReturnType()) { 11 | // alias to class for Symfony 6 on PHP 8+ using native types like `start(): bool` 12 | class_alias(__NAMESPACE__ . '\\VirtualSessionStorageForSymfony6', __NAMESPACE__ . '\\VirtualSessionStorage'); 13 | } else { 14 | // fall back to class without native types 15 | 16 | class VirtualSessionStorage extends NativeSessionStorage { 17 | /** 18 | * @var \Ratchet\Session\Serialize\HandlerInterface 19 | */ 20 | protected $_serializer; 21 | 22 | /** 23 | * @param \SessionHandlerInterface $handler 24 | * @param string $sessionId The ID of the session to retrieve 25 | * @param \Ratchet\Session\Serialize\HandlerInterface $serializer 26 | */ 27 | public function __construct(\SessionHandlerInterface $handler, $sessionId, HandlerInterface $serializer) { 28 | $this->setSaveHandler($handler); 29 | $this->saveHandler->setId($sessionId); 30 | $this->_serializer = $serializer; 31 | $this->setMetadataBag(null); 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | public function start() { 38 | if ($this->started && !$this->closed) { 39 | return true; 40 | } 41 | 42 | // You have to call Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler::open() to use 43 | // pdo_sqlite (and possible pdo_*) as session storage, if you are using a DSN string instead of a \PDO object 44 | // in the constructor. The method arguments are filled with the values, which are also used by the symfony 45 | // framework in this case. This must not be the best choice, but it works. 46 | $this->saveHandler->open(session_save_path(), session_name()); 47 | 48 | $rawData = $this->saveHandler->read($this->saveHandler->getId()); 49 | $sessionData = $this->_serializer->unserialize($rawData); 50 | 51 | $this->loadSession($sessionData); 52 | 53 | if (!$this->saveHandler->isWrapper() && !$this->saveHandler->isSessionHandlerInterface()) { 54 | $this->saveHandler->setActive(false); 55 | } 56 | 57 | return true; 58 | } 59 | 60 | /** 61 | * {@inheritdoc} 62 | */ 63 | public function regenerate($destroy = false, $lifetime = null) { 64 | // .. ? 65 | return false; 66 | } 67 | 68 | /** 69 | * {@inheritdoc} 70 | */ 71 | public function save() { 72 | // get the data from the bags? 73 | // serialize the data 74 | // save the data using the saveHandler 75 | // $this->saveHandler->write($this->saveHandler->getId(), 76 | 77 | if (!$this->saveHandler->isWrapper() && !$this->getSaveHandler()->isSessionHandlerInterface()) { 78 | $this->saveHandler->setActive(false); 79 | } 80 | 81 | $this->closed = true; 82 | } 83 | 84 | /** 85 | * {@inheritdoc} 86 | */ 87 | public function setSaveHandler($saveHandler = null) { 88 | if (!($saveHandler instanceof \SessionHandlerInterface)) { 89 | throw new \InvalidArgumentException('Handler must be instance of SessionHandlerInterface'); 90 | } 91 | 92 | if (!($saveHandler instanceof VirtualProxy)) { 93 | $saveHandler = new VirtualProxy($saveHandler); 94 | } 95 | 96 | $this->saveHandler = $saveHandler; 97 | } 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /src/Ratchet/Session/Storage/VirtualSessionStorageForSymfony6.php: -------------------------------------------------------------------------------- 1 | setSaveHandler($handler); 26 | $this->saveHandler->setId($sessionId); 27 | $this->_serializer = $serializer; 28 | $this->setMetadataBag(null); 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | public function start(): bool { 35 | if ($this->started && !$this->closed) { 36 | return true; 37 | } 38 | 39 | // You have to call Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler::open() to use 40 | // pdo_sqlite (and possible pdo_*) as session storage, if you are using a DSN string instead of a \PDO object 41 | // in the constructor. The method arguments are filled with the values, which are also used by the symfony 42 | // framework in this case. This must not be the best choice, but it works. 43 | $this->saveHandler->open(session_save_path(), session_name()); 44 | 45 | $rawData = $this->saveHandler->read($this->saveHandler->getId()); 46 | $sessionData = $this->_serializer->unserialize($rawData); 47 | 48 | $this->loadSession($sessionData); 49 | 50 | if (!$this->saveHandler->isWrapper() && !$this->saveHandler->isSessionHandlerInterface()) { 51 | $this->saveHandler->setActive(false); 52 | } 53 | 54 | return true; 55 | } 56 | 57 | /** 58 | * {@inheritdoc} 59 | */ 60 | public function regenerate(bool $destroy = false, ?int $lifetime = null): bool { 61 | // .. ? 62 | return false; 63 | } 64 | 65 | /** 66 | * {@inheritdoc} 67 | */ 68 | public function save() { 69 | // get the data from the bags? 70 | // serialize the data 71 | // save the data using the saveHandler 72 | // $this->saveHandler->write($this->saveHandler->getId(), 73 | 74 | if (!$this->saveHandler->isWrapper() && !$this->getSaveHandler()->isSessionHandlerInterface()) { 75 | $this->saveHandler->setActive(false); 76 | } 77 | 78 | $this->closed = true; 79 | } 80 | 81 | /** 82 | * {@inheritdoc} 83 | */ 84 | public function setSaveHandler($saveHandler = null) { 85 | if (!($saveHandler instanceof \SessionHandlerInterface)) { 86 | throw new \InvalidArgumentException('Handler must be instance of SessionHandlerInterface'); 87 | } 88 | 89 | if (!($saveHandler instanceof VirtualProxy)) { 90 | $saveHandler = new VirtualProxy($saveHandler); 91 | } 92 | 93 | $this->saveHandler = $saveHandler; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Ratchet/Session/Storage/VirtualSessionStorageForSymfony7.php: -------------------------------------------------------------------------------- 1 | setSaveHandler($handler); 27 | $this->saveHandler->setId($sessionId); 28 | $this->_serializer = $serializer; 29 | $this->setMetadataBag(null); 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | public function start(): bool { 36 | if ($this->started && !$this->closed) { 37 | return true; 38 | } 39 | 40 | // You have to call Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler::open() to use 41 | // pdo_sqlite (and possible pdo_*) as session storage, if you are using a DSN string instead of a \PDO object 42 | // in the constructor. The method arguments are filled with the values, which are also used by the symfony 43 | // framework in this case. This must not be the best choice, but it works. 44 | $this->saveHandler->open(session_save_path(), session_name()); 45 | 46 | $rawData = $this->saveHandler->read($this->saveHandler->getId()); 47 | $sessionData = $this->_serializer->unserialize($rawData); 48 | 49 | $this->loadSession($sessionData); 50 | 51 | if (!$this->saveHandler->isWrapper() && !$this->saveHandler->isSessionHandlerInterface()) { 52 | $this->saveHandler->setActive(false); 53 | } 54 | 55 | return true; 56 | } 57 | 58 | /** 59 | * {@inheritdoc} 60 | */ 61 | public function regenerate(bool $destroy = false, ?int $lifetime = null): bool { 62 | // .. ? 63 | return false; 64 | } 65 | 66 | /** 67 | * {@inheritdoc} 68 | */ 69 | public function save(): void { 70 | // get the data from the bags? 71 | // serialize the data 72 | // save the data using the saveHandler 73 | // $this->saveHandler->write($this->saveHandler->getId(), 74 | 75 | if (!$this->saveHandler->isWrapper() && !$this->getSaveHandler()->isSessionHandlerInterface()) { 76 | $this->saveHandler->setActive(false); 77 | } 78 | 79 | $this->closed = true; 80 | } 81 | 82 | /** 83 | * {@inheritdoc} 84 | */ 85 | public function setSaveHandler(AbstractProxy|\SessionHandlerInterface|null $saveHandler): void { 86 | if (!($saveHandler instanceof \SessionHandlerInterface)) { 87 | throw new \InvalidArgumentException('Handler must be instance of SessionHandlerInterface'); 88 | } 89 | 90 | if (!($saveHandler instanceof VirtualProxy)) { 91 | $saveHandler = new VirtualProxy($saveHandler); 92 | } 93 | 94 | $this->saveHandler = $saveHandler; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Ratchet/Wamp/Exception.php: -------------------------------------------------------------------------------- 1 | _decorating = $serverComponent; 53 | $this->connections = new \SplObjectStorage; 54 | } 55 | 56 | /** 57 | * {@inheritdoc} 58 | */ 59 | public function getSubProtocols() { 60 | if ($this->_decorating instanceof WsServerInterface) { 61 | $subs = $this->_decorating->getSubProtocols(); 62 | $subs[] = 'wamp'; 63 | 64 | return $subs; 65 | } 66 | 67 | return ['wamp']; 68 | } 69 | 70 | /** 71 | * {@inheritdoc} 72 | */ 73 | public function onOpen(ConnectionInterface $conn) { 74 | $decor = new WampConnection($conn); 75 | $this->connections->attach($conn, $decor); 76 | 77 | $this->_decorating->onOpen($decor); 78 | } 79 | 80 | /** 81 | * {@inheritdoc} 82 | * @throws \Ratchet\Wamp\Exception 83 | * @throws \Ratchet\Wamp\JsonException 84 | */ 85 | public function onMessage(ConnectionInterface $from, $msg) { 86 | $from = $this->connections[$from]; 87 | 88 | if (null === ($json = @json_decode($msg, true))) { 89 | throw new JsonException; 90 | } 91 | 92 | if (!is_array($json) || $json !== array_values($json)) { 93 | throw new Exception("Invalid WAMP message format"); 94 | } 95 | 96 | if (isset($json[1]) && !(is_string($json[1]) || is_numeric($json[1]))) { 97 | throw new Exception('Invalid Topic, must be a string'); 98 | } 99 | 100 | switch ($json[0]) { 101 | case static::MSG_PREFIX: 102 | $from->WAMP->prefixes[$json[1]] = $json[2]; 103 | break; 104 | 105 | case static::MSG_CALL: 106 | array_shift($json); 107 | $callID = array_shift($json); 108 | $procURI = array_shift($json); 109 | 110 | if (count($json) == 1 && is_array($json[0])) { 111 | $json = $json[0]; 112 | } 113 | 114 | $this->_decorating->onCall($from, $callID, $from->getUri($procURI), $json); 115 | break; 116 | 117 | case static::MSG_SUBSCRIBE: 118 | $this->_decorating->onSubscribe($from, $from->getUri($json[1])); 119 | break; 120 | 121 | case static::MSG_UNSUBSCRIBE: 122 | $this->_decorating->onUnSubscribe($from, $from->getUri($json[1])); 123 | break; 124 | 125 | case static::MSG_PUBLISH: 126 | $exclude = (array_key_exists(3, $json) ? $json[3] : null); 127 | if (!is_array($exclude)) { 128 | if (true === (boolean)$exclude) { 129 | $exclude = [$from->WAMP->sessionId]; 130 | } else { 131 | $exclude = []; 132 | } 133 | } 134 | 135 | $eligible = (array_key_exists(4, $json) ? $json[4] : []); 136 | 137 | $this->_decorating->onPublish($from, $from->getUri($json[1]), $json[2], $exclude, $eligible); 138 | break; 139 | 140 | default: 141 | throw new Exception('Invalid WAMP message type'); 142 | } 143 | } 144 | 145 | /** 146 | * {@inheritdoc} 147 | */ 148 | public function onClose(ConnectionInterface $conn) { 149 | $decor = $this->connections[$conn]; 150 | $this->connections->detach($conn); 151 | 152 | $this->_decorating->onClose($decor); 153 | } 154 | 155 | /** 156 | * {@inheritdoc} 157 | */ 158 | public function onError(ConnectionInterface $conn, \Exception $e) { 159 | return $this->_decorating->onError($this->connections[$conn], $e); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/Ratchet/Wamp/Topic.php: -------------------------------------------------------------------------------- 1 | id = $topicId; 18 | $this->subscribers = new \SplObjectStorage; 19 | } 20 | 21 | /** 22 | * @return string 23 | */ 24 | public function getId() { 25 | return $this->id; 26 | } 27 | 28 | public function __toString() { 29 | return $this->getId(); 30 | } 31 | 32 | /** 33 | * Send a message to all the connections in this topic 34 | * @param string|array $msg Payload to publish 35 | * @param array $exclude A list of session IDs the message should be excluded from (blacklist) 36 | * @param array $eligible A list of session Ids the message should be send to (whitelist) 37 | * @return Topic The same Topic object to chain 38 | */ 39 | public function broadcast($msg, array $exclude = array(), array $eligible = array()) { 40 | $useEligible = (bool)count($eligible); 41 | foreach ($this->subscribers as $client) { 42 | if (in_array($client->WAMP->sessionId, $exclude)) { 43 | continue; 44 | } 45 | 46 | if ($useEligible && !in_array($client->WAMP->sessionId, $eligible)) { 47 | continue; 48 | } 49 | 50 | $client->event($this->id, $msg); 51 | } 52 | 53 | return $this; 54 | } 55 | 56 | /** 57 | * @param WampConnection $conn 58 | * @return boolean 59 | */ 60 | public function has(ConnectionInterface $conn) { 61 | return $this->subscribers->contains($conn); 62 | } 63 | 64 | /** 65 | * @param WampConnection $conn 66 | * @return Topic 67 | */ 68 | public function add(ConnectionInterface $conn) { 69 | $this->subscribers->attach($conn); 70 | 71 | return $this; 72 | } 73 | 74 | /** 75 | * @param WampConnection $conn 76 | * @return Topic 77 | */ 78 | public function remove(ConnectionInterface $conn) { 79 | if ($this->subscribers->contains($conn)) { 80 | $this->subscribers->detach($conn); 81 | } 82 | 83 | return $this; 84 | } 85 | 86 | /** 87 | * {@inheritdoc} 88 | */ 89 | #[\ReturnTypeWillChange] 90 | public function getIterator() { 91 | return $this->subscribers; 92 | } 93 | 94 | /** 95 | * {@inheritdoc} 96 | */ 97 | #[\ReturnTypeWillChange] 98 | public function count() { 99 | return $this->subscribers->count(); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Ratchet/Wamp/TopicManager.php: -------------------------------------------------------------------------------- 1 | app = $app; 19 | } 20 | 21 | /** 22 | * {@inheritdoc} 23 | */ 24 | public function onOpen(ConnectionInterface $conn) { 25 | $conn->WAMP->subscriptions = new \SplObjectStorage; 26 | $this->app->onOpen($conn); 27 | } 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | public function onCall(ConnectionInterface $conn, $id, $topic, array $params) { 33 | $this->app->onCall($conn, $id, $this->getTopic($topic), $params); 34 | } 35 | 36 | /** 37 | * {@inheritdoc} 38 | */ 39 | public function onSubscribe(ConnectionInterface $conn, $topic) { 40 | $topicObj = $this->getTopic($topic); 41 | 42 | if ($conn->WAMP->subscriptions->contains($topicObj)) { 43 | return; 44 | } 45 | 46 | $this->topicLookup[$topic]->add($conn); 47 | $conn->WAMP->subscriptions->attach($topicObj); 48 | $this->app->onSubscribe($conn, $topicObj); 49 | } 50 | 51 | /** 52 | * {@inheritdoc} 53 | */ 54 | public function onUnsubscribe(ConnectionInterface $conn, $topic) { 55 | $topicObj = $this->getTopic($topic); 56 | 57 | if (!$conn->WAMP->subscriptions->contains($topicObj)) { 58 | return; 59 | } 60 | 61 | $this->cleanTopic($topicObj, $conn); 62 | 63 | $this->app->onUnsubscribe($conn, $topicObj); 64 | } 65 | 66 | /** 67 | * {@inheritdoc} 68 | */ 69 | public function onPublish(ConnectionInterface $conn, $topic, $event, array $exclude, array $eligible) { 70 | $this->app->onPublish($conn, $this->getTopic($topic), $event, $exclude, $eligible); 71 | } 72 | 73 | /** 74 | * {@inheritdoc} 75 | */ 76 | public function onClose(ConnectionInterface $conn) { 77 | $this->app->onClose($conn); 78 | 79 | foreach ($this->topicLookup as $topic) { 80 | $this->cleanTopic($topic, $conn); 81 | } 82 | } 83 | 84 | /** 85 | * {@inheritdoc} 86 | */ 87 | public function onError(ConnectionInterface $conn, \Exception $e) { 88 | $this->app->onError($conn, $e); 89 | } 90 | 91 | /** 92 | * {@inheritdoc} 93 | */ 94 | public function getSubProtocols() { 95 | if ($this->app instanceof WsServerInterface) { 96 | return $this->app->getSubProtocols(); 97 | } 98 | 99 | return array(); 100 | } 101 | 102 | /** 103 | * @param string 104 | * @return Topic 105 | */ 106 | protected function getTopic($topic) { 107 | if (!array_key_exists($topic, $this->topicLookup)) { 108 | $this->topicLookup[$topic] = new Topic($topic); 109 | } 110 | 111 | return $this->topicLookup[$topic]; 112 | } 113 | 114 | protected function cleanTopic(Topic $topic, ConnectionInterface $conn) { 115 | if ($conn->WAMP->subscriptions->contains($topic)) { 116 | $conn->WAMP->subscriptions->detach($topic); 117 | } 118 | 119 | $this->topicLookup[$topic->getId()]->remove($conn); 120 | 121 | if (0 === $topic->count()) { 122 | unset($this->topicLookup[$topic->getId()]); 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Ratchet/Wamp/WampConnection.php: -------------------------------------------------------------------------------- 1 | WAMP = new \StdClass; 20 | $this->WAMP->sessionId = str_replace('.', '', uniqid(mt_rand(), true)); 21 | $this->WAMP->prefixes = array(); 22 | 23 | $this->send(json_encode(array(WAMP::MSG_WELCOME, $this->WAMP->sessionId, 1, \Ratchet\VERSION))); 24 | } 25 | 26 | /** 27 | * Successfully respond to a call made by the client 28 | * @param string $id The unique ID given by the client to respond to 29 | * @param array $data an object or array 30 | * @return WampConnection 31 | */ 32 | public function callResult($id, $data = array()) { 33 | return $this->send(json_encode(array(WAMP::MSG_CALL_RESULT, $id, $data))); 34 | } 35 | 36 | /** 37 | * Respond with an error to a client call 38 | * @param string $id The unique ID given by the client to respond to 39 | * @param string $errorUri The URI given to identify the specific error 40 | * @param string $desc A developer-oriented description of the error 41 | * @param string $details An optional human readable detail message to send back 42 | * @return WampConnection 43 | */ 44 | public function callError($id, $errorUri, $desc = '', $details = null) { 45 | if ($errorUri instanceof Topic) { 46 | $errorUri = (string)$errorUri; 47 | } 48 | 49 | $data = array(WAMP::MSG_CALL_ERROR, $id, $errorUri, $desc); 50 | 51 | if (null !== $details) { 52 | $data[] = $details; 53 | } 54 | 55 | return $this->send(json_encode($data)); 56 | } 57 | 58 | /** 59 | * @param string $topic The topic to broadcast to 60 | * @param mixed $msg Data to send with the event. Anything that is json'able 61 | * @return WampConnection 62 | */ 63 | public function event($topic, $msg) { 64 | return $this->send(json_encode(array(WAMP::MSG_EVENT, (string)$topic, $msg))); 65 | } 66 | 67 | /** 68 | * @param string $curie 69 | * @param string $uri 70 | * @return WampConnection 71 | */ 72 | public function prefix($curie, $uri) { 73 | $this->WAMP->prefixes[$curie] = (string)$uri; 74 | 75 | return $this->send(json_encode(array(WAMP::MSG_PREFIX, $curie, (string)$uri))); 76 | } 77 | 78 | /** 79 | * Get the full request URI from the connection object if a prefix has been established for it 80 | * @param string $uri 81 | * @return string 82 | */ 83 | public function getUri($uri) { 84 | $curieSeperator = ':'; 85 | 86 | if (preg_match('/http(s*)\:\/\//', $uri) == false) { 87 | if (strpos($uri, $curieSeperator) !== false) { 88 | list($prefix, $action) = explode($curieSeperator, $uri); 89 | 90 | if(isset($this->WAMP->prefixes[$prefix]) === true){ 91 | return $this->WAMP->prefixes[$prefix] . '#' . $action; 92 | } 93 | } 94 | } 95 | 96 | return $uri; 97 | } 98 | 99 | /** 100 | * @internal 101 | */ 102 | public function send($data) { 103 | $this->getConnection()->send($data); 104 | 105 | return $this; 106 | } 107 | 108 | /** 109 | * {@inheritdoc} 110 | */ 111 | public function close($opt = null) { 112 | $this->getConnection()->close($opt); 113 | } 114 | 115 | } 116 | -------------------------------------------------------------------------------- /src/Ratchet/Wamp/WampServer.php: -------------------------------------------------------------------------------- 1 | wampProtocol = new ServerProtocol(new TopicManager($app)); 27 | } 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | public function onOpen(ConnectionInterface $conn) { 33 | $this->wampProtocol->onOpen($conn); 34 | } 35 | 36 | /** 37 | * {@inheritdoc} 38 | */ 39 | public function onMessage(ConnectionInterface $conn, $msg) { 40 | try { 41 | $this->wampProtocol->onMessage($conn, $msg); 42 | } catch (Exception $we) { 43 | $conn->close(1007); 44 | } 45 | } 46 | 47 | /** 48 | * {@inheritdoc} 49 | */ 50 | public function onClose(ConnectionInterface $conn) { 51 | $this->wampProtocol->onClose($conn); 52 | } 53 | 54 | /** 55 | * {@inheritdoc} 56 | */ 57 | public function onError(ConnectionInterface $conn, \Exception $e) { 58 | $this->wampProtocol->onError($conn, $e); 59 | } 60 | 61 | /** 62 | * {@inheritdoc} 63 | */ 64 | public function getSubProtocols() { 65 | return $this->wampProtocol->getSubProtocols(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Ratchet/Wamp/WampServerInterface.php: -------------------------------------------------------------------------------- 1 | connection = $conn; 18 | $this->buffer = $buffer; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Ratchet/WebSocket/MessageCallableInterface.php: -------------------------------------------------------------------------------- 1 | WebSocket->closing) { 17 | if (!($msg instanceof DataInterface)) { 18 | $msg = new Frame($msg); 19 | } 20 | 21 | $this->getConnection()->send($msg->getContents()); 22 | } 23 | 24 | return $this; 25 | } 26 | 27 | /** 28 | * @param int|\Ratchet\RFC6455\Messaging\DataInterface 29 | */ 30 | public function close($code = 1000) { 31 | if ($this->WebSocket->closing) { 32 | return; 33 | } 34 | 35 | if ($code instanceof DataInterface) { 36 | $this->send($code); 37 | } else { 38 | $this->send(new Frame(pack('n', $code), true, Frame::OP_CLOSE)); 39 | } 40 | 41 | $this->getConnection()->close(); 42 | 43 | $this->WebSocket->closing = true; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Ratchet/WebSocket/WsServer.php: -------------------------------------------------------------------------------- 1 | msgCb = function(ConnectionInterface $conn, MessageInterface $msg) { 71 | $this->delegate->onMessage($conn, $msg); 72 | }; 73 | } elseif ($component instanceof DataComponentInterface) { 74 | $this->msgCb = function(ConnectionInterface $conn, MessageInterface $msg) { 75 | $this->delegate->onMessage($conn, $msg->getPayload()); 76 | }; 77 | } else { 78 | throw new \UnexpectedValueException('Expected instance of \Ratchet\WebSocket\MessageComponentInterface or \Ratchet\MessageComponentInterface'); 79 | } 80 | 81 | if (bin2hex('✓') !== 'e29c93') { 82 | throw new \DomainException('Bad encoding, unicode character ✓ did not match expected value. Ensure charset UTF-8 and check ini val mbstring.func_autoload'); 83 | } 84 | 85 | $this->delegate = $component; 86 | $this->connections = new \SplObjectStorage; 87 | 88 | $this->closeFrameChecker = new CloseFrameChecker; 89 | $this->handshakeNegotiator = new ServerNegotiator(new RequestVerifier); 90 | $this->handshakeNegotiator->setStrictSubProtocolCheck(true); 91 | 92 | if ($component instanceof WsServerInterface) { 93 | $this->handshakeNegotiator->setSupportedSubProtocols($component->getSubProtocols()); 94 | } 95 | 96 | $this->pongReceiver = function() {}; 97 | 98 | $reusableUnderflowException = new \UnderflowException; 99 | $this->ueFlowFactory = function() use ($reusableUnderflowException) { 100 | return $reusableUnderflowException; 101 | }; 102 | } 103 | 104 | /** 105 | * {@inheritdoc} 106 | */ 107 | #[HackSupportForPHP8] public function onOpen(ConnectionInterface $conn, ?RequestInterface $request = null) { /* 108 | public function onOpen(ConnectionInterface $conn, RequestInterface $request = null) { /**/ 109 | if (null === $request) { 110 | throw new \UnexpectedValueException('$request can not be null'); 111 | } 112 | 113 | $conn->httpRequest = $request; 114 | 115 | $conn->WebSocket = new \StdClass; 116 | $conn->WebSocket->closing = false; 117 | 118 | $response = $this->handshakeNegotiator->handshake($request)->withHeader('X-Powered-By', \Ratchet\VERSION); 119 | 120 | $conn->send(Message::toString($response)); 121 | 122 | if (101 !== $response->getStatusCode()) { 123 | return $conn->close(); 124 | } 125 | 126 | $wsConn = new WsConnection($conn); 127 | 128 | $streamer = new MessageBuffer( 129 | $this->closeFrameChecker, 130 | function(MessageInterface $msg) use ($wsConn) { 131 | $cb = $this->msgCb; 132 | $cb($wsConn, $msg); 133 | }, 134 | function(FrameInterface $frame) use ($wsConn) { 135 | $this->onControlFrame($frame, $wsConn); 136 | }, 137 | true, 138 | $this->ueFlowFactory 139 | ); 140 | 141 | $this->connections->attach($conn, new ConnContext($wsConn, $streamer)); 142 | 143 | return $this->delegate->onOpen($wsConn); 144 | } 145 | 146 | /** 147 | * {@inheritdoc} 148 | */ 149 | public function onMessage(ConnectionInterface $from, $msg) { 150 | if ($from->WebSocket->closing) { 151 | return; 152 | } 153 | 154 | $this->connections[$from]->buffer->onData($msg); 155 | } 156 | 157 | /** 158 | * {@inheritdoc} 159 | */ 160 | public function onClose(ConnectionInterface $conn) { 161 | if ($this->connections->contains($conn)) { 162 | $context = $this->connections[$conn]; 163 | $this->connections->detach($conn); 164 | 165 | $this->delegate->onClose($context->connection); 166 | } 167 | } 168 | 169 | /** 170 | * {@inheritdoc} 171 | */ 172 | public function onError(ConnectionInterface $conn, \Exception $e) { 173 | if ($this->connections->contains($conn)) { 174 | $this->delegate->onError($this->connections[$conn]->connection, $e); 175 | } else { 176 | $conn->close(); 177 | } 178 | } 179 | 180 | public function onControlFrame(FrameInterface $frame, WsConnection $conn) { 181 | switch ($frame->getOpCode()) { 182 | case Frame::OP_CLOSE: 183 | $conn->close($frame); 184 | break; 185 | case Frame::OP_PING: 186 | $conn->send(new Frame($frame->getPayload(), true, Frame::OP_PONG)); 187 | break; 188 | case Frame::OP_PONG: 189 | $pongReceiver = $this->pongReceiver; 190 | $pongReceiver($frame, $conn); 191 | break; 192 | } 193 | } 194 | 195 | public function setStrictSubProtocolCheck($enable) { 196 | $this->handshakeNegotiator->setStrictSubProtocolCheck($enable); 197 | } 198 | 199 | public function enableKeepAlive(LoopInterface $loop, $interval = 30) { 200 | $lastPing = new Frame(uniqid(), true, Frame::OP_PING); 201 | $pingedConnections = new \SplObjectStorage; 202 | $splClearer = new \SplObjectStorage; 203 | 204 | $this->pongReceiver = function(FrameInterface $frame, $wsConn) use ($pingedConnections, &$lastPing) { 205 | if ($frame->getPayload() === $lastPing->getPayload()) { 206 | $pingedConnections->detach($wsConn); 207 | } 208 | }; 209 | 210 | $loop->addPeriodicTimer((int)$interval, function() use ($pingedConnections, &$lastPing, $splClearer) { 211 | foreach ($pingedConnections as $wsConn) { 212 | $wsConn->close(); 213 | } 214 | $pingedConnections->removeAllExcept($splClearer); 215 | 216 | $lastPing = new Frame(uniqid(), true, Frame::OP_PING); 217 | 218 | foreach ($this->connections as $key => $conn) { 219 | $wsConn = $this->connections[$conn]->connection; 220 | 221 | $wsConn->send($lastPing); 222 | $pingedConnections->attach($wsConn); 223 | } 224 | }); 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /src/Ratchet/WebSocket/WsServerInterface.php: -------------------------------------------------------------------------------- 1 | send($msg); 9 | } 10 | 11 | public function onOpen(ConnectionInterface $conn) { 12 | } 13 | 14 | public function onClose(ConnectionInterface $conn) { 15 | } 16 | 17 | public function onError(ConnectionInterface $conn, \Exception $e) { 18 | } 19 | } 20 | 21 | $port = $argc > 1 ? $argv[1] : 8000; 22 | $impl = sprintf('React\EventLoop\%sLoop', $argc > 2 ? $argv[2] : 'StreamSelect'); 23 | 24 | $loop = new $impl; 25 | 26 | // prefer SocketServer (reactphp/socket v1.9+) over legacy \React\Socket\Server 27 | $sock = class_exists('React\Socket\SocketServer') ? new React\Socket\SocketServer('0.0.0.0:' . $port, [], $loop) : new React\Socket\Server('0.0.0.0:' . $port, $loop); 28 | 29 | $wsServer = new Ratchet\WebSocket\WsServer(new BinaryEcho); 30 | // This is enabled to test https://github.com/ratchetphp/Ratchet/issues/430 31 | // The time is left at 10 minutes so that it will not try to every ping anything 32 | // This causes the Ratchet server to crash on test 2.7 33 | $wsServer->enableKeepAlive($loop, 600); 34 | 35 | $app = new Ratchet\Http\HttpServer($wsServer); 36 | 37 | $server = new Ratchet\Server\IoServer($app, $sock, $loop); 38 | $server->run(); 39 | -------------------------------------------------------------------------------- /tests/autobahn/fuzzingclient-all.json: -------------------------------------------------------------------------------- 1 | { 2 | "options": {"failByDrop": false} 3 | , "outdir": "reports/ab" 4 | 5 | , "servers": [ 6 | {"agent": "Ratchet/0.4 libevent", "url": "ws://localhost:8001", "options": {"version": 18}} 7 | , {"agent": "Ratchet/0.4 libev", "url": "ws://localhost:8004", "options": {"version": 18}} 8 | , {"agent": "Ratchet/0.4 streams", "url": "ws://localhost:8002", "options": {"version": 18}} 9 | , {"agent": "AutobahnTestSuite/0.5.9", "url": "ws://localhost:8000", "options": {"version": 18}} 10 | ] 11 | 12 | , "cases": ["*"] 13 | , "exclude-cases": [] 14 | , "exclude-agent-cases": {} 15 | } 16 | -------------------------------------------------------------------------------- /tests/autobahn/fuzzingclient-profile.json: -------------------------------------------------------------------------------- 1 | { 2 | "options": {"failByDrop": false} 3 | , "outdir": "reports/profile" 4 | 5 | , "servers": [ 6 | {"agent": "Ratchet", "url": "ws://localhost:8000", "options": {"version": 18}} 7 | ] 8 | 9 | , "cases": ["9.7.4"] 10 | , "exclude-cases": ["1.2.*", "2.3", "2.4", "2.6", "9.2.*", "9.4.*", "9.6.*", "9.8.*"] 11 | , "exclude-agent-cases": {} 12 | } 13 | -------------------------------------------------------------------------------- /tests/autobahn/fuzzingclient-quick.json: -------------------------------------------------------------------------------- 1 | { 2 | "options": {"failByDrop": false} 3 | , "outdir": "reports/rfc" 4 | 5 | , "servers": [ 6 | {"agent": "Ratchet", "url": "ws://localhost:8000", "options": {"version": 18}} 7 | ] 8 | 9 | , "cases": ["*"] 10 | , "exclude-cases": [] 11 | , "exclude-agent-cases": {} 12 | } 13 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | addPsr4('Ratchet\\', __DIR__ . '/helpers/Ratchet'); 5 | -------------------------------------------------------------------------------- /tests/helpers/Ratchet/AbstractMessageComponentTestCase.php: -------------------------------------------------------------------------------- 1 | _app = $this->getMockBuilder($this->getComponentClassString())->getMock(); 20 | $decorator = $this->getDecoratorClassString(); 21 | $this->_serv = new $decorator($this->_app); 22 | $this->_conn = $this->getMockBuilder('Ratchet\Mock\Connection')->getMock(); 23 | 24 | $this->doOpen($this->_conn); 25 | } 26 | 27 | protected function doOpen($conn) { 28 | $this->_serv->onOpen($conn); 29 | } 30 | 31 | public function isExpectedConnection() { 32 | return $this->isInstanceOf($this->getConnectionClassString()); 33 | } 34 | 35 | public function testOpen() { 36 | $this->_app->expects($this->once())->method('onOpen')->with($this->isExpectedConnection()); 37 | $this->doOpen($this->getMockBuilder('Ratchet\Mock\Connection')->getMock()); 38 | } 39 | 40 | public function testOnClose() { 41 | $this->_app->expects($this->once())->method('onClose')->with($this->isExpectedConnection()); 42 | $this->_serv->onClose($this->_conn); 43 | } 44 | 45 | public function testOnError() { 46 | $e = new \Exception('Whoops!'); 47 | $this->_app->expects($this->once())->method('onError')->with($this->isExpectedConnection(), $e); 48 | $this->_serv->onError($this->_conn, $e); 49 | } 50 | 51 | public function passthroughMessageTest($value) { 52 | $this->_app->expects($this->once())->method('onMessage')->with($this->isExpectedConnection(), $value); 53 | $this->_serv->onMessage($this->_conn, $value); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/helpers/Ratchet/Mock/Connection.php: -------------------------------------------------------------------------------- 1 | '' 9 | , 'close' => false 10 | ); 11 | 12 | public $remoteAddress = '127.0.0.1'; 13 | 14 | public function send($data) { 15 | $this->last[__FUNCTION__] = $data; 16 | } 17 | 18 | public function close() { 19 | $this->last[__FUNCTION__] = true; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/helpers/Ratchet/Mock/ConnectionDecorator.php: -------------------------------------------------------------------------------- 1 | '' 8 | , 'end' => false 9 | ); 10 | 11 | public function send($data) { 12 | $this->last[__FUNCTION__] = $data; 13 | 14 | $this->getConnection()->send($data); 15 | } 16 | 17 | public function close() { 18 | $this->last[__FUNCTION__] = true; 19 | 20 | $this->getConnection()->close(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/helpers/Ratchet/Mock/WampComponent.php: -------------------------------------------------------------------------------- 1 | protocols; 14 | } 15 | 16 | public function onCall(ConnectionInterface $conn, $id, $procURI, array $params) { 17 | $this->last[__FUNCTION__] = func_get_args(); 18 | } 19 | 20 | public function onSubscribe(ConnectionInterface $conn, $topic) { 21 | $this->last[__FUNCTION__] = func_get_args(); 22 | } 23 | 24 | public function onUnSubscribe(ConnectionInterface $conn, $topic) { 25 | $this->last[__FUNCTION__] = func_get_args(); 26 | } 27 | 28 | public function onPublish(ConnectionInterface $conn, $topic, $event, array $exclude, array $eligible) { 29 | $this->last[__FUNCTION__] = func_get_args(); 30 | } 31 | 32 | public function onOpen(ConnectionInterface $conn) { 33 | $this->last[__FUNCTION__] = func_get_args(); 34 | } 35 | 36 | public function onClose(ConnectionInterface $conn) { 37 | $this->last[__FUNCTION__] = func_get_args(); 38 | } 39 | 40 | public function onError(ConnectionInterface $conn, \Exception $e) { 41 | $this->last[__FUNCTION__] = func_get_args(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/helpers/Ratchet/NullComponent.php: -------------------------------------------------------------------------------- 1 | mock = $this->getMockBuilder('Ratchet\Mock\Connection')->getMock(); 20 | $this->l1 = new ConnectionDecorator($this->mock); 21 | $this->l2 = new ConnectionDecorator($this->l1); 22 | } 23 | 24 | public function testGet() { 25 | $var = 'hello'; 26 | $val = 'world'; 27 | 28 | $this->mock->$var = $val; 29 | 30 | $this->assertEquals($val, $this->l1->$var); 31 | $this->assertEquals($val, $this->l2->$var); 32 | } 33 | 34 | public function testSet() { 35 | $var = 'Chris'; 36 | $val = 'Boden'; 37 | 38 | $this->l1->$var = $val; 39 | 40 | $this->assertEquals($val, $this->mock->$var); 41 | } 42 | 43 | public function testSetLevel2() { 44 | $var = 'Try'; 45 | $val = 'Again'; 46 | 47 | $this->l2->$var = $val; 48 | 49 | $this->assertEquals($val, $this->mock->$var); 50 | } 51 | 52 | public function testIsSetTrue() { 53 | $var = 'PHP'; 54 | $val = 'Ratchet'; 55 | 56 | $this->mock->$var = $val; 57 | 58 | $this->assertTrue(isset($this->l1->$var)); 59 | $this->assertTrue(isset($this->l2->$var)); 60 | } 61 | 62 | public function testIsSetFalse() { 63 | $var = 'herp'; 64 | $val = 'derp'; 65 | 66 | $this->assertFalse(isset($this->l1->$var)); 67 | $this->assertFalse(isset($this->l2->$var)); 68 | } 69 | 70 | public function testUnset() { 71 | $var = 'Flying'; 72 | $val = 'Monkey'; 73 | 74 | $this->mock->$var = $val; 75 | unset($this->l1->$var); 76 | 77 | $this->assertFalse(isset($this->mock->$var)); 78 | } 79 | 80 | public function testUnsetLevel2() { 81 | $var = 'Flying'; 82 | $val = 'Monkey'; 83 | 84 | $this->mock->$var = $val; 85 | unset($this->l2->$var); 86 | 87 | $this->assertFalse(isset($this->mock->$var)); 88 | } 89 | 90 | public function testGetConnection() { 91 | $class = new \ReflectionClass('Ratchet\\AbstractConnectionDecorator'); 92 | $method = $class->getMethod('getConnection'); 93 | $method->setAccessible(true); 94 | 95 | $conn = $method->invokeArgs($this->l1, array()); 96 | 97 | $this->assertSame($this->mock, $conn); 98 | } 99 | 100 | public function testGetConnectionLevel2() { 101 | $class = new \ReflectionClass('Ratchet\\AbstractConnectionDecorator'); 102 | $method = $class->getMethod('getConnection'); 103 | $method->setAccessible(true); 104 | 105 | $conn = $method->invokeArgs($this->l2, array()); 106 | 107 | $this->assertSame($this->l1, $conn); 108 | } 109 | 110 | public function testWrapperCanStoreSelfInDecorator() { 111 | $this->mock->decorator = $this->l1; 112 | 113 | $this->assertSame($this->l1, $this->l2->decorator); 114 | } 115 | 116 | public function testDecoratorRecursion() { 117 | $this->mock->decorator = new \stdClass; 118 | $this->mock->decorator->conn = $this->l1; 119 | 120 | $this->assertSame($this->l1, $this->mock->decorator->conn); 121 | $this->assertSame($this->l1, $this->l1->decorator->conn); 122 | $this->assertSame($this->l1, $this->l2->decorator->conn); 123 | } 124 | 125 | public function testDecoratorRecursionLevel2() { 126 | $this->mock->decorator = new \stdClass; 127 | $this->mock->decorator->conn = $this->l2; 128 | 129 | $this->assertSame($this->l2, $this->mock->decorator->conn); 130 | $this->assertSame($this->l2, $this->l1->decorator->conn); 131 | $this->assertSame($this->l2, $this->l2->decorator->conn); 132 | 133 | // just for fun 134 | $this->assertSame($this->l2, $this->l2->decorator->conn->decorator->conn->decorator->conn); 135 | } 136 | 137 | public function testWarningGettingNothing() { 138 | $error = false; 139 | set_error_handler(function () use (&$error) { 140 | $error = true; 141 | }, PHP_VERSION_ID >= 80000 ? E_WARNING : E_NOTICE); 142 | 143 | $var = $this->mock->nonExistant; 144 | 145 | restore_error_handler(); 146 | 147 | $this->assertTrue($error); 148 | $this->assertNull($var); 149 | } 150 | 151 | public function testWarningGettingNothingLevel1() { 152 | $error = false; 153 | set_error_handler(function () use (&$error) { 154 | $error = true; 155 | }, PHP_VERSION_ID >= 80000 ? E_WARNING : E_NOTICE); 156 | 157 | $var = $this->l1->nonExistant; 158 | 159 | restore_error_handler(); 160 | 161 | $this->assertTrue($error); 162 | $this->assertNull($var); 163 | } 164 | 165 | public function testWarningGettingNothingLevel2() { 166 | $error = false; 167 | set_error_handler(function () use (&$error) { 168 | $error = true; 169 | }, PHP_VERSION_ID >= 80000 ? E_WARNING : E_NOTICE); 170 | 171 | $var = $this->l2->nonExistant; 172 | 173 | restore_error_handler(); 174 | 175 | $this->assertTrue($error); 176 | $this->assertNull($var); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /tests/unit/AppTest.php: -------------------------------------------------------------------------------- 1 | expectException('InvalidArgumentException'); 11 | $this->expectExceptionMessage('Argument #4 ($loop) expected null|React\EventLoop\LoopInterface'); 12 | } else { 13 | $this->setExpectedException('InvalidArgumentException', 'Argument #4 ($loop) expected null|React\EventLoop\LoopInterface'); 14 | } 15 | new App('localhost', 8080, '127.0.0.1', 'loop'); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/unit/Http/HttpRequestParserTest.php: -------------------------------------------------------------------------------- 1 | parser = new HttpRequestParser; 17 | } 18 | 19 | public function headersProvider() { 20 | return array( 21 | array(false, "GET / HTTP/1.1\r\nHost: socketo.me\r\n") 22 | , array(true, "GET / HTTP/1.1\r\nHost: socketo.me\r\n\r\n") 23 | , array(true, "GET / HTTP/1.1\r\nHost: socketo.me\r\n\r\n1") 24 | , array(true, "GET / HTTP/1.1\r\nHost: socketo.me\r\n\r\nHixie✖") 25 | , array(true, "GET / HTTP/1.1\r\nHost: socketo.me\r\n\r\nHixie✖\r\n\r\n") 26 | , array(true, "GET / HTTP/1.1\r\nHost: socketo.me\r\n\r\nHixie\r\n") 27 | ); 28 | } 29 | 30 | /** 31 | * @dataProvider headersProvider 32 | */ 33 | public function testIsEom($expected, $message) { 34 | $this->assertEquals($expected, $this->parser->isEom($message)); 35 | } 36 | 37 | public function testBufferOverflowResponse() { 38 | $conn = $this->getMockBuilder('Ratchet\Mock\Connection')->getMock(); 39 | 40 | $this->parser->maxSize = 20; 41 | 42 | $this->assertNull($this->parser->onMessage($conn, "GET / HTTP/1.1\r\n")); 43 | 44 | if (method_exists($this, 'expectException')) { 45 | $this->expectException('OverflowException'); 46 | } else { 47 | $this->setExpectedException('OverflowException'); 48 | } 49 | 50 | $this->parser->onMessage($conn, "Header-Is: Too Big"); 51 | } 52 | 53 | public function testReturnTypeIsRequest() { 54 | $conn = $this->getMockBuilder('Ratchet\Mock\Connection')->getMock(); 55 | $return = $this->parser->onMessage($conn, "GET / HTTP/1.1\r\nHost: socketo.me\r\n\r\n"); 56 | 57 | $this->assertInstanceOf('Psr\Http\Message\RequestInterface', $return); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tests/unit/Http/HttpServerTest.php: -------------------------------------------------------------------------------- 1 | _conn->httpHeadersReceived = true; 15 | } 16 | 17 | public function getConnectionClassString() { 18 | return 'Ratchet\ConnectionInterface'; 19 | } 20 | 21 | public function getDecoratorClassString() { 22 | return 'Ratchet\Http\HttpServer'; 23 | } 24 | 25 | public function getComponentClassString() { 26 | return 'Ratchet\Http\HttpServerInterface'; 27 | } 28 | 29 | public function testOpen() { 30 | $headers = "GET / HTTP/1.1\r\nHost: socketo.me\r\n\r\n"; 31 | 32 | $this->_conn->httpHeadersReceived = false; 33 | $this->_app->expects($this->once())->method('onOpen')->with($this->isExpectedConnection()); 34 | $this->_serv->onMessage($this->_conn, $headers); 35 | } 36 | 37 | public function testOnMessageAfterHeaders() { 38 | $headers = "GET / HTTP/1.1\r\nHost: socketo.me\r\n\r\n"; 39 | $this->_conn->httpHeadersReceived = false; 40 | $this->_serv->onMessage($this->_conn, $headers); 41 | 42 | $message = "Hello World!"; 43 | $this->_app->expects($this->once())->method('onMessage')->with($this->isExpectedConnection(), $message); 44 | $this->_serv->onMessage($this->_conn, $message); 45 | } 46 | 47 | public function testBufferOverflow() { 48 | $this->_conn->expects($this->once())->method('close'); 49 | $this->_conn->httpHeadersReceived = false; 50 | 51 | $this->_serv->onMessage($this->_conn, str_repeat('a', 5000)); 52 | } 53 | 54 | public function testCloseIfNotEstablished() { 55 | $this->_conn->httpHeadersReceived = false; 56 | $this->_conn->expects($this->once())->method('close'); 57 | $this->_serv->onError($this->_conn, new \Exception('Whoops!')); 58 | } 59 | 60 | public function testBufferHeaders() { 61 | $this->_conn->httpHeadersReceived = false; 62 | $this->_app->expects($this->never())->method('onOpen'); 63 | $this->_app->expects($this->never())->method('onMessage'); 64 | 65 | $this->_serv->onMessage($this->_conn, "GET / HTTP/1.1"); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /tests/unit/Http/OriginCheckTest.php: -------------------------------------------------------------------------------- 1 | _reqStub = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); 16 | $this->_reqStub->expects($this->any())->method('getHeader')->will($this->returnValue(['localhost'])); 17 | 18 | parent::setUpConnection(); 19 | 20 | assert($this->_serv instanceof OriginCheck); 21 | $this->_serv->allowedOrigins[] = 'localhost'; 22 | } 23 | 24 | protected function doOpen($conn) { 25 | $this->_serv->onOpen($conn, $this->_reqStub); 26 | } 27 | 28 | public function getConnectionClassString() { 29 | return 'Ratchet\ConnectionInterface'; 30 | } 31 | 32 | public function getDecoratorClassString() { 33 | return 'Ratchet\Http\OriginCheck'; 34 | } 35 | 36 | public function getComponentClassString() { 37 | return 'Ratchet\Http\HttpServerInterface'; 38 | } 39 | 40 | public function testCloseOnNonMatchingOrigin() { 41 | $this->_serv->allowedOrigins = ['socketo.me']; 42 | $this->_conn->expects($this->once())->method('close'); 43 | 44 | $this->_serv->onOpen($this->_conn, $this->_reqStub); 45 | } 46 | 47 | public function testOnMessage() { 48 | $this->passthroughMessageTest('Hello World!'); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/unit/Http/RouterTest.php: -------------------------------------------------------------------------------- 1 | _conn = $this->getMockBuilder('Ratchet\Mock\Connection')->getMock(); 26 | $this->_uri = $this->getMockBuilder('Psr\Http\Message\UriInterface')->getMock(); 27 | $this->_req = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); 28 | $this->_req 29 | ->expects($this->any()) 30 | ->method('getUri') 31 | ->will($this->returnValue($this->_uri)); 32 | $this->_matcher = $this->getMockBuilder('Symfony\Component\Routing\Matcher\UrlMatcherInterface')->getMock(); 33 | $this->_matcher 34 | ->expects($this->any()) 35 | ->method('getContext') 36 | ->will($this->returnValue($this->getMockBuilder('Symfony\Component\Routing\RequestContext')->getMock())); 37 | $this->_router = new Router($this->_matcher); 38 | 39 | $this->_uri->expects($this->any())->method('getPath')->will($this->returnValue('ws://doesnt.matter/')); 40 | $this->_uri->expects($this->any())->method('withQuery')->with($this->callback(function($val) { 41 | $this->setResult($val); 42 | 43 | return true; 44 | }))->will($this->returnSelf()); 45 | $this->_uri->expects($this->any())->method('getHost')->willReturn('example.com'); 46 | $this->_req->expects($this->any())->method('withUri')->will($this->returnSelf()); 47 | $this->_req->expects($this->any())->method('getMethod')->willReturn('GET'); 48 | } 49 | 50 | public function testFourOhFour() { 51 | $this->_conn->expects($this->once())->method('close'); 52 | 53 | $nope = new ResourceNotFoundException; 54 | $this->_matcher->expects($this->any())->method('match')->will($this->throwException($nope)); 55 | 56 | $this->_router->onOpen($this->_conn, $this->_req); 57 | } 58 | 59 | public function testNullRequest() { 60 | if (method_exists($this, 'expectException')) { 61 | $this->expectException('UnexpectedValueException'); 62 | } else { 63 | $this->setExpectedException('UnexpectedValueException'); 64 | } 65 | $this->_router->onOpen($this->_conn); 66 | } 67 | 68 | public function testControllerIsMessageComponentInterface() { 69 | if (method_exists($this, 'expectException')) { 70 | $this->expectException('UnexpectedValueException'); 71 | } else { 72 | $this->setExpectedException('UnexpectedValueException'); 73 | } 74 | $this->_matcher->expects($this->any())->method('match')->will($this->returnValue(array('_controller' => new \StdClass))); 75 | $this->_router->onOpen($this->_conn, $this->_req); 76 | } 77 | 78 | public function testControllerOnOpen() { 79 | $controller = $this->getMockBuilder('Ratchet\WebSocket\WsServer')->disableOriginalConstructor()->getMock(); 80 | $this->_matcher->expects($this->any())->method('match')->will($this->returnValue(array('_controller' => $controller))); 81 | $this->_router->onOpen($this->_conn, $this->_req); 82 | 83 | $expectedConn = $this->isInstanceOf('Ratchet\Mock\Connection'); 84 | $controller->expects($this->once())->method('onOpen')->with($expectedConn, $this->_req); 85 | 86 | $this->_matcher->expects($this->any())->method('match')->will($this->returnValue(array('_controller' => $controller))); 87 | $this->_router->onOpen($this->_conn, $this->_req); 88 | } 89 | 90 | public function testControllerOnMessageBubbles() { 91 | $message = "The greatest trick the Devil ever pulled was convincing the world he didn't exist"; 92 | $controller = $this->getMockBuilder('Ratchet\WebSocket\WsServer')->disableOriginalConstructor()->getMock(); 93 | $controller->expects($this->once())->method('onMessage')->with($this->_conn, $message); 94 | 95 | $this->_conn->controller = $controller; 96 | 97 | $this->_router->onMessage($this->_conn, $message); 98 | } 99 | 100 | public function testControllerOnCloseBubbles() { 101 | $controller = $this->getMockBuilder('Ratchet\WebSocket\WsServer')->disableOriginalConstructor()->getMock(); 102 | $controller->expects($this->once())->method('onClose')->with($this->_conn); 103 | 104 | $this->_conn->controller = $controller; 105 | 106 | $this->_router->onClose($this->_conn); 107 | } 108 | 109 | public function testControllerOnErrorBubbles() { 110 | $e= new \Exception('One cannot be betrayed if one has no exceptions'); 111 | $controller = $this->getMockBuilder('Ratchet\WebSocket\WsServer')->disableOriginalConstructor()->getMock(); 112 | $controller->expects($this->once())->method('onError')->with($this->_conn, $e); 113 | 114 | $this->_conn->controller = $controller; 115 | 116 | $this->_router->onError($this->_conn, $e); 117 | } 118 | 119 | public function testRouterGeneratesRouteParameters() { 120 | /** @var $controller WsServerInterface */ 121 | $controller = $this->getMockBuilder('Ratchet\WebSocket\WsServer')->disableOriginalConstructor()->getMock(); 122 | /** @var $matcher UrlMatcherInterface */ 123 | $this->_matcher->expects($this->any())->method('match')->will( 124 | $this->returnValue(['_controller' => $controller, 'foo' => 'bar', 'baz' => 'qux']) 125 | ); 126 | $conn = $this->getMockBuilder('Ratchet\Mock\Connection')->getMock(); 127 | 128 | $this->_uri->expects($this->once())->method('withQuery')->with('foo=bar&baz=qux')->willReturnSelf(); 129 | $this->_req->expects($this->once())->method('withUri')->will($this->returnSelf()); 130 | 131 | $router = new Router($this->_matcher); 132 | 133 | $router->onOpen($conn, $this->_req); 134 | } 135 | 136 | public function testQueryParams() { 137 | $controller = $this->getMockBuilder('Ratchet\WebSocket\WsServer')->disableOriginalConstructor()->getMock(); 138 | $this->_matcher->expects($this->any())->method('match')->will( 139 | $this->returnValue(['_controller' => $controller, 'foo' => 'bar', 'baz' => 'qux']) 140 | ); 141 | 142 | $conn = $this->getMockBuilder('Ratchet\Mock\Connection')->getMock(); 143 | $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); 144 | $uri = new \GuzzleHttp\Psr7\Uri('ws://doesnt.matter/endpoint?hello=world&foo=nope'); 145 | 146 | $request->expects($this->any())->method('getUri')->will($this->returnCallback(function() use (&$uri) { 147 | return $uri; 148 | })); 149 | $request->expects($this->any())->method('withUri')->with($this->callback(function($url) use (&$uri) { 150 | $uri = $url; 151 | 152 | return true; 153 | }))->will($this->returnSelf()); 154 | $request->expects($this->once())->method('getMethod')->willReturn('GET'); 155 | 156 | $router = new Router($this->_matcher); 157 | $router->onOpen($conn, $request); 158 | 159 | $this->assertEquals('foo=nope&baz=qux&hello=world', $request->getUri()->getQuery()); 160 | $this->assertEquals('ws', $request->getUri()->getScheme()); 161 | $this->assertEquals('doesnt.matter', $request->getUri()->getHost()); 162 | } 163 | 164 | public function testImpatientClientOverflow() { 165 | $this->_conn->expects($this->once())->method('close'); 166 | 167 | $header = "GET /nope HTTP/1.1 168 | Upgrade: websocket 169 | Connection: upgrade 170 | Host: localhost 171 | Origin: http://localhost 172 | Sec-WebSocket-Version: 13\r\n\r\n"; 173 | 174 | $app = new HttpServer(new Router(new UrlMatcher(new RouteCollection, new RequestContext))); 175 | $app->onOpen($this->_conn); 176 | $app->onMessage($this->_conn, $header); 177 | $app->onMessage($this->_conn, 'Silly body'); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /tests/unit/Server/EchoServerTest.php: -------------------------------------------------------------------------------- 1 | _conn = $this->getMockBuilder('Ratchet\ConnectionInterface')->getMock(); 15 | $this->_comp = new EchoServer; 16 | } 17 | 18 | public function testMessageEchod() { 19 | $message = 'Tillsonburg, my back still aches when I hear that word.'; 20 | $this->_conn->expects($this->once())->method('send')->with($message); 21 | $this->_comp->onMessage($this->_conn, $message); 22 | } 23 | 24 | public function testErrorClosesConnection() { 25 | ob_start(); 26 | $this->_conn->expects($this->once())->method('close'); 27 | $this->_comp->onError($this->_conn, new \Exception); 28 | ob_end_clean(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/unit/Server/FlashPolicyComponentTest.php: -------------------------------------------------------------------------------- 1 | _policy = new FlashPolicy(); 18 | } 19 | 20 | public function testPolicyRender() { 21 | $this->_policy->setSiteControl('all'); 22 | $this->_policy->addAllowedAccess('example.com', '*'); 23 | $this->_policy->addAllowedAccess('dev.example.com', '*'); 24 | 25 | $this->assertInstanceOf('SimpleXMLElement', $this->_policy->renderPolicy()); 26 | } 27 | 28 | public function testInvalidPolicyReader() { 29 | if (method_exists($this, 'expectException')) { 30 | $this->expectException('UnexpectedValueException'); 31 | } else { 32 | $this->setExpectedException('UnexpectedValueException'); 33 | } 34 | $this->_policy->renderPolicy(); 35 | } 36 | 37 | public function testInvalidDomainPolicyReader() { 38 | if (method_exists($this, 'expectException')) { 39 | $this->expectException('UnexpectedValueException'); 40 | } else { 41 | $this->setExpectedException('UnexpectedValueException'); 42 | } 43 | $this->_policy->setSiteControl('all'); 44 | $this->_policy->addAllowedAccess('dev.example.*', '*'); 45 | $this->_policy->renderPolicy(); 46 | } 47 | 48 | /** 49 | * @dataProvider siteControl 50 | */ 51 | public function testSiteControlValidation($accept, $permittedCrossDomainPolicies) { 52 | $this->assertEquals($accept, $this->_policy->validateSiteControl($permittedCrossDomainPolicies)); 53 | } 54 | 55 | public static function siteControl() { 56 | return array( 57 | array(true, 'all') 58 | , array(true, 'none') 59 | , array(true, 'master-only') 60 | , array(false, 'by-content-type') 61 | , array(false, 'by-ftp-filename') 62 | , array(false, '') 63 | , array(false, 'all ') 64 | , array(false, 'asdf') 65 | , array(false, '@893830') 66 | , array(false, '*') 67 | ); 68 | } 69 | 70 | /** 71 | * @dataProvider URI 72 | */ 73 | public function testDomainValidation($accept, $domain) { 74 | $this->assertEquals($accept, $this->_policy->validateDomain($domain)); 75 | } 76 | 77 | public static function URI() { 78 | return array( 79 | array(true, '*') 80 | , array(true, 'example.com') 81 | , array(true, 'exam-ple.com') 82 | , array(true, '*.example.com') 83 | , array(true, 'www.example.com') 84 | , array(true, 'dev.dev.example.com') 85 | , array(true, 'http://example.com') 86 | , array(true, 'https://example.com') 87 | , array(true, 'http://*.example.com') 88 | , array(false, 'exam*ple.com') 89 | , array(true, '127.0.255.1') 90 | , array(true, 'localhost') 91 | , array(false, 'www.example.*') 92 | , array(false, 'www.exa*le.com') 93 | , array(false, 'www.example.*com') 94 | , array(false, '*.example.*') 95 | , array(false, 'gasldf*$#a0sdf0a8sdf') 96 | ); 97 | } 98 | 99 | /** 100 | * @dataProvider ports 101 | */ 102 | public function testPortValidation($accept, $ports) { 103 | $this->assertEquals($accept, $this->_policy->validatePorts($ports)); 104 | } 105 | 106 | public static function ports() { 107 | return array( 108 | array(true, '*') 109 | , array(true, '80') 110 | , array(true, '80,443') 111 | , array(true, '507,516-523') 112 | , array(true, '507,516-523,333') 113 | , array(true, '507,516-523,507,516-523') 114 | , array(false, '516-') 115 | , array(true, '516-523,11') 116 | , array(false, '516,-523,11') 117 | , array(false, 'example') 118 | , array(false, 'asdf,123') 119 | , array(false, '--') 120 | , array(false, ',,,') 121 | , array(false, '838*') 122 | ); 123 | } 124 | 125 | public function testAddAllowedAccessOnlyAcceptsValidPorts() { 126 | if (method_exists($this, 'expectException')) { 127 | $this->expectException('UnexpectedValueException'); 128 | } else { 129 | $this->setExpectedException('UnexpectedValueException'); 130 | } 131 | 132 | $this->_policy->addAllowedAccess('*', 'nope'); 133 | } 134 | 135 | public function testSetSiteControlThrowsException() { 136 | if (method_exists($this, 'expectException')) { 137 | $this->expectException('UnexpectedValueException'); 138 | } else { 139 | $this->setExpectedException('UnexpectedValueException'); 140 | } 141 | 142 | $this->_policy->setSiteControl('nope'); 143 | } 144 | 145 | public function testErrorClosesConnection() { 146 | $conn = $this->getMockBuilder('Ratchet\\ConnectionInterface')->getMock(); 147 | $conn->expects($this->once())->method('close'); 148 | 149 | $this->_policy->onError($conn, new \Exception); 150 | } 151 | 152 | public function testOnMessageSendsString() { 153 | $this->_policy->addAllowedAccess('*', '*'); 154 | 155 | $conn = $this->getMockBuilder('Ratchet\\ConnectionInterface')->getMock(); 156 | $conn->expects($this->once())->method('send')->with($this->isType('string')); 157 | 158 | $this->_policy->onMessage($conn, ' '); 159 | } 160 | 161 | public function testOnOpenExists() { 162 | $this->assertTrue(method_exists($this->_policy, 'onOpen')); 163 | $conn = $this->getMockBuilder('Ratchet\ConnectionInterface')->getMock(); 164 | $this->_policy->onOpen($conn); 165 | } 166 | 167 | public function testOnCloseExists() { 168 | $this->assertTrue(method_exists($this->_policy, 'onClose')); 169 | $conn = $this->getMockBuilder('Ratchet\ConnectionInterface')->getMock(); 170 | $this->_policy->onClose($conn); 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /tests/unit/Server/IoConnectionTest.php: -------------------------------------------------------------------------------- 1 | sock = $this->getMockBuilder('React\\Socket\\ConnectionInterface')->getMock(); 18 | $this->conn = new IoConnection($this->sock); 19 | } 20 | 21 | public function testCloseBubbles() { 22 | $this->sock->expects($this->once())->method('end'); 23 | $this->conn->close(); 24 | } 25 | 26 | public function testSendBubbles() { 27 | $msg = '6 hour rides are productive'; 28 | 29 | $this->sock->expects($this->once())->method('write')->with($msg); 30 | $this->conn->send($msg); 31 | } 32 | 33 | public function testSendReturnsSelf() { 34 | $this->assertSame($this->conn, $this->conn->send('fluent interface')); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/unit/Server/IoServerTest.php: -------------------------------------------------------------------------------- 1 | futureTick(function () use ($loop) { 23 | $loop->stop(); 24 | }); 25 | 26 | $loop->run(); 27 | } 28 | 29 | /** 30 | * @before 31 | */ 32 | public function setUpServer() { 33 | $this->app = $this->getMockBuilder('Ratchet\\MessageComponentInterface')->getMock(); 34 | 35 | $loop = new StreamSelectLoop; 36 | 37 | // prefer SocketServer (reactphp/socket v1.9+) over legacy \React\Socket\Server 38 | $this->reactor = class_exists('React\Socket\SocketServer') ? new SocketServer('127.0.0.1:0', [], $loop) : new LegacySocketServer('127.0.0.1:0', $loop); 39 | 40 | $uri = $this->reactor->getAddress(); 41 | $this->port = parse_url((strpos($uri, '://') === false ? 'tcp://' : '') . $uri, PHP_URL_PORT); 42 | $this->server = new IoServer($this->app, $this->reactor, $loop); 43 | } 44 | 45 | public function testCtorThrowsForInvalidLoop() { 46 | if (method_exists($this, 'expectException')) { 47 | $this->expectException('InvalidArgumentException'); 48 | $this->expectExceptionMessage('Argument #3 ($loop) expected null|React\EventLoop\LoopInterface'); 49 | } else { 50 | $this->setExpectedException('InvalidArgumentException', 'Argument #3 ($loop) expected null|React\EventLoop\LoopInterface'); 51 | } 52 | new IoServer($this->app, $this->reactor, 'loop'); 53 | } 54 | 55 | public function testOnOpen() { 56 | $this->app->expects($this->once())->method('onOpen')->with($this->isInstanceOf('Ratchet\\ConnectionInterface')); 57 | 58 | $client = stream_socket_client("tcp://localhost:{$this->port}"); 59 | 60 | $this->tickLoop($this->server->loop); 61 | 62 | //$this->assertTrue(is_string($this->app->last['onOpen'][0]->remoteAddress)); 63 | //$this->assertTrue(is_int($this->app->last['onOpen'][0]->resourceId)); 64 | } 65 | 66 | /** 67 | * @requires extension sockets 68 | */ 69 | public function testOnData() { 70 | $msg = 'Hello World!'; 71 | 72 | $this->app->expects($this->once())->method('onMessage')->with( 73 | $this->isInstanceOf('Ratchet\\ConnectionInterface') 74 | , $msg 75 | ); 76 | 77 | $client = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); 78 | socket_set_option($client, SOL_SOCKET, SO_REUSEADDR, 1); 79 | socket_set_option($client, SOL_SOCKET, SO_SNDBUF, 4096); 80 | socket_set_block($client); 81 | socket_connect($client, 'localhost', $this->port); 82 | 83 | $this->tickLoop($this->server->loop); 84 | 85 | socket_write($client, $msg); 86 | $this->tickLoop($this->server->loop); 87 | 88 | socket_shutdown($client, 1); 89 | socket_shutdown($client, 0); 90 | socket_close($client); 91 | 92 | $this->tickLoop($this->server->loop); 93 | } 94 | 95 | /** 96 | * @requires extension sockets 97 | */ 98 | public function testOnClose() { 99 | $this->app->expects($this->once())->method('onClose')->with($this->isInstanceOf('Ratchet\\ConnectionInterface')); 100 | 101 | $client = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); 102 | socket_set_option($client, SOL_SOCKET, SO_REUSEADDR, 1); 103 | socket_set_option($client, SOL_SOCKET, SO_SNDBUF, 4096); 104 | socket_set_block($client); 105 | socket_connect($client, 'localhost', $this->port); 106 | 107 | $this->tickLoop($this->server->loop); 108 | 109 | socket_shutdown($client, 1); 110 | socket_shutdown($client, 0); 111 | socket_close($client); 112 | 113 | $this->tickLoop($this->server->loop); 114 | } 115 | 116 | public function testFactory() { 117 | $server = IoServer::factory($this->app, 0); 118 | $server->socket->close(); 119 | 120 | $this->assertInstanceOf('Ratchet\\Server\\IoServer', $server); 121 | } 122 | 123 | public function testNoLoopProvidedError() { 124 | if (method_exists($this, 'expectException')) { 125 | $this->expectException('RuntimeException'); 126 | } else { 127 | $this->setExpectedException('RuntimeException'); 128 | } 129 | 130 | $io = new IoServer($this->app, $this->reactor); 131 | $io->run(); 132 | } 133 | 134 | public function testOnErrorPassesException() { 135 | $conn = $this->getMockBuilder('React\\Socket\\ConnectionInterface')->getMock(); 136 | 137 | // assign dynamic property without raising notice on PHP 8.2+ 138 | set_error_handler(function () { }, E_DEPRECATED); 139 | $conn->decor = $this->getMockBuilder('Ratchet\\ConnectionInterface')->getMock(); 140 | restore_error_handler(); 141 | 142 | $err = new \Exception("Nope"); 143 | 144 | $this->app->expects($this->once())->method('onError')->with($conn->decor, $err); 145 | 146 | $this->server->handleError($err, $conn); 147 | } 148 | 149 | public function onErrorCalledWhenExceptionThrown() { 150 | $this->markTestIncomplete("Need to learn how to throw an exception from a mock"); 151 | 152 | $conn = $this->getMockBuilder('React\\Socket\\ConnectionInterface')->getMock(); 153 | $this->server->handleConnect($conn); 154 | 155 | $e = new \Exception; 156 | $this->app->expects($this->once())->method('onMessage')->with($this->isInstanceOf('Ratchet\\ConnectionInterface'), 'f')->will($e); 157 | $this->app->expects($this->once())->method('onError')->with($this->instanceOf('Ratchet\\ConnectionInterface', $e)); 158 | 159 | $this->server->handleData('f', $conn); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /tests/unit/Server/IpBlackListComponentTest.php: -------------------------------------------------------------------------------- 1 | mock = $this->getMockBuilder('Ratchet\\MessageComponentInterface')->getMock(); 18 | $this->blocker = new IpBlackList($this->mock); 19 | } 20 | 21 | public function testOnOpen() { 22 | $this->mock->expects($this->exactly(3))->method('onOpen'); 23 | 24 | $conn1 = $this->newConn(); 25 | $conn2 = $this->newConn(); 26 | $conn3 = $this->newConn(); 27 | 28 | $this->blocker->onOpen($conn1); 29 | $this->blocker->onOpen($conn3); 30 | $this->blocker->onOpen($conn2); 31 | } 32 | 33 | public function testBlockDoesNotTriggerOnOpen() { 34 | $conn = $this->newConn(); 35 | 36 | $this->blocker->blockAddress($conn->remoteAddress); 37 | 38 | $this->mock->expects($this->never())->method('onOpen'); 39 | 40 | $ret = $this->blocker->onOpen($conn); 41 | } 42 | 43 | public function testBlockDoesNotTriggerOnClose() { 44 | $conn = $this->newConn(); 45 | 46 | $this->blocker->blockAddress($conn->remoteAddress); 47 | 48 | $this->mock->expects($this->never())->method('onClose'); 49 | 50 | $ret = $this->blocker->onOpen($conn); 51 | } 52 | 53 | public function testOnMessageDecoration() { 54 | $conn = $this->newConn(); 55 | $msg = 'Hello not being blocked'; 56 | 57 | $this->mock->expects($this->once())->method('onMessage')->with($conn, $msg); 58 | 59 | $this->blocker->onMessage($conn, $msg); 60 | } 61 | 62 | public function testOnCloseDecoration() { 63 | $conn = $this->newConn(); 64 | 65 | $this->mock->expects($this->once())->method('onClose')->with($conn); 66 | 67 | $this->blocker->onClose($conn); 68 | } 69 | 70 | public function testBlockClosesConnection() { 71 | $conn = $this->newConn(); 72 | $this->blocker->blockAddress($conn->remoteAddress); 73 | 74 | $conn->expects($this->once())->method('close'); 75 | 76 | $this->blocker->onOpen($conn); 77 | } 78 | 79 | public function testAddAndRemoveWithFluentInterfaces() { 80 | $blockOne = '127.0.0.1'; 81 | $blockTwo = '192.168.1.1'; 82 | $unblock = '75.119.207.140'; 83 | 84 | $this->blocker 85 | ->blockAddress($unblock) 86 | ->blockAddress($blockOne) 87 | ->unblockAddress($unblock) 88 | ->blockAddress($blockTwo) 89 | ; 90 | 91 | $this->assertEquals(array($blockOne, $blockTwo), $this->blocker->getBlockedAddresses()); 92 | } 93 | 94 | public function testDecoratorPassesErrors() { 95 | $conn = $this->newConn(); 96 | $e = new \Exception('I threw an error'); 97 | 98 | $this->mock->expects($this->once())->method('onError')->with($conn, $e); 99 | 100 | $this->blocker->onError($conn, $e); 101 | } 102 | 103 | public function addressProvider() { 104 | return array( 105 | array('127.0.0.1', '127.0.0.1') 106 | , array('localhost', 'localhost') 107 | , array('fe80::1%lo0', 'fe80::1%lo0') 108 | , array('127.0.0.1', '127.0.0.1:6392') 109 | ); 110 | } 111 | 112 | /** 113 | * @dataProvider addressProvider 114 | */ 115 | public function testFilterAddress($expected, $input) { 116 | $this->assertEquals($expected, $this->blocker->filterAddress($input)); 117 | } 118 | 119 | public function testUnblockingSilentlyFails() { 120 | $this->assertInstanceOf('Ratchet\\Server\\IpBlackList', $this->blocker->unblockAddress('localhost')); 121 | } 122 | 123 | protected function newConn() { 124 | $conn = $this->getMockBuilder('Ratchet\Mock\Connection')->getMock(); 125 | $conn->remoteAddress = '127.0.0.1'; 126 | 127 | return $conn; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /tests/unit/Session/Serialize/PhpBinaryHandlerTest.php: -------------------------------------------------------------------------------- 1 | _handler = new PhpBinaryHandler; 17 | } 18 | 19 | public function serializedProvider() { 20 | return array( 21 | array( 22 | "\x0f" . '_sf2_attributes' . 'a:2:{s:5:"hello";s:5:"world";s:4:"last";i:1332872102;}' . "\x0c" . '_sf2_flashes' . 'a:0:{}' 23 | , array( 24 | '_sf2_attributes' => array( 25 | 'hello' => 'world' 26 | , 'last' => 1332872102 27 | ) 28 | , '_sf2_flashes' => array() 29 | ) 30 | ) 31 | ); 32 | } 33 | 34 | /** 35 | * @dataProvider serializedProvider 36 | */ 37 | public function testUnserialize($in, $expected) { 38 | $this->assertEquals($expected, $this->_handler->unserialize($in)); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/unit/Session/Serialize/PhpHandlerTest.php: -------------------------------------------------------------------------------- 1 | _handler = new PhpHandler; 17 | } 18 | 19 | public function serializedProvider() { 20 | return array( 21 | array( 22 | '_sf2_attributes|a:2:{s:5:"hello";s:5:"world";s:4:"last";i:1332872102;}_sf2_flashes|a:0:{}' 23 | , array( 24 | '_sf2_attributes' => array( 25 | 'hello' => 'world' 26 | , 'last' => 1332872102 27 | ) 28 | , '_sf2_flashes' => array() 29 | ) 30 | ) 31 | ); 32 | } 33 | 34 | /** 35 | * @dataProvider serializedProvider 36 | */ 37 | public function testUnserialize($in, $expected) { 38 | $this->assertEquals($expected, $this->_handler->unserialize($in)); 39 | } 40 | 41 | /** 42 | * @dataProvider serializedProvider 43 | */ 44 | public function testSerialize($serialized, $original) { 45 | $this->assertEquals($serialized, $this->_handler->serialize($original)); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/unit/Session/SessionProviderTest.php: -------------------------------------------------------------------------------- 1 | markTestIncomplete('Test needs to be updated for ini_set issue in PHP 7.2'); 18 | 19 | if (!class_exists('Symfony\Component\HttpFoundation\Session\Session')) { 20 | return $this->markTestSkipped('Dependency of Symfony HttpFoundation failed'); 21 | } 22 | 23 | parent::setUpConnection(); 24 | $this->_serv = new SessionProvider($this->_app, new NullSessionHandler); 25 | } 26 | 27 | public function testCtorThrowsForInvalidSerializer() { 28 | if (method_exists($this, 'expectException')) { 29 | $this->expectException('InvalidArgumentException'); 30 | $this->expectExceptionMessage('Argument #4 ($serializer) expected null|Ratchet\Session\Serialize\HandlerInterface'); 31 | } else { 32 | $this->setExpectedException('InvalidArgumentException', 'Argument #4 ($serializer) expected null|Ratchet\Session\Serialize\HandlerInterface'); 33 | } 34 | new SessionProvider($this->_app, new NullSessionHandler(), [], 'serializer'); 35 | } 36 | 37 | /** 38 | * @after 39 | */ 40 | public function tearDownHandler() { 41 | ini_set('session.serialize_handler', 'php'); 42 | } 43 | 44 | public function getConnectionClassString() { 45 | return 'Ratchet\ConnectionInterface'; 46 | } 47 | 48 | public function getDecoratorClassString() { 49 | return 'Ratchet\NullComponent'; 50 | } 51 | 52 | public function getComponentClassString() { 53 | return 'Ratchet\Http\HttpServerInterface'; 54 | } 55 | 56 | public function classCaseProvider() { 57 | return array( 58 | array('php', 'Php') 59 | , array('php_binary', 'PhpBinary') 60 | ); 61 | } 62 | 63 | /** 64 | * @dataProvider classCaseProvider 65 | */ 66 | public function testToClassCase($in, $out) { 67 | $ref = new \ReflectionClass('Ratchet\\Session\\SessionProvider'); 68 | $method = $ref->getMethod('toClassCase'); 69 | $method->setAccessible(true); 70 | 71 | $component = new SessionProvider($this->getMockBuilder($this->getComponentClassString())->getMock(), $this->getMockBuilder('SessionHandlerInterface')->getMock()); 72 | $this->assertEquals($out, $method->invokeArgs($component, array($in))); 73 | } 74 | 75 | /** 76 | * I think I have severely butchered this test...it's not so much of a unit test as it is a full-fledged component test 77 | */ 78 | public function testConnectionValueFromPdo() { 79 | if (!extension_loaded('PDO') || !extension_loaded('pdo_sqlite')) { 80 | return $this->markTestSkipped('Session test requires PDO and pdo_sqlite'); 81 | } 82 | 83 | $sessionId = md5('testSession'); 84 | 85 | $dbOptions = array( 86 | 'db_table' => 'sessions' 87 | , 'db_id_col' => 'sess_id' 88 | , 'db_data_col' => 'sess_data' 89 | , 'db_time_col' => 'sess_time' 90 | , 'db_lifetime_col' => 'sess_lifetime' 91 | ); 92 | 93 | $pdo = new \PDO("sqlite::memory:"); 94 | $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); 95 | $pdo->exec(vsprintf("CREATE TABLE %s (%s TEXT NOT NULL PRIMARY KEY, %s BLOB NOT NULL, %s INTEGER NOT NULL, %s INTEGER)", $dbOptions)); 96 | 97 | $pdoHandler = new PdoSessionHandler($pdo, $dbOptions); 98 | $pdoHandler->write($sessionId, '_sf2_attributes|a:2:{s:5:"hello";s:5:"world";s:4:"last";i:1332872102;}_sf2_flashes|a:0:{}'); 99 | 100 | $component = new SessionProvider($this->getMockBuilder($this->getComponentClassString())->getMock(), $pdoHandler, array('auto_start' => 1)); 101 | $connection = $this->getMockBuilder('Ratchet\\ConnectionInterface')->getMock(); 102 | 103 | $headers = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); 104 | $headers->expects($this->once())->method('getHeader')->will($this->returnValue([ini_get('session.name') . "={$sessionId};"])); 105 | 106 | $component->onOpen($connection, $headers); 107 | 108 | $this->assertEquals('world', $connection->Session->get('hello')); 109 | } 110 | 111 | protected function newConn() { 112 | $conn = $this->getMockBuilder('Ratchet\ConnectionInterface')->getMock(); 113 | 114 | $headers = $this->getMockBuilder('Psr\Http\Message\Request', array('getCookie'), array('POST', '/', array()))->getMock(); 115 | $headers->expects($this->once())->method('getCookie', array(ini_get('session.name')))->will($this->returnValue(null)); 116 | 117 | return $conn; 118 | } 119 | 120 | public function testOnMessageDecorator() { 121 | $message = "Database calls are usually blocking :("; 122 | $this->_app->expects($this->once())->method('onMessage')->with($this->isExpectedConnection(), $message); 123 | $this->_serv->onMessage($this->_conn, $message); 124 | } 125 | 126 | public function testRejectInvalidSeralizers() { 127 | if (!function_exists('wddx_serialize_value')) { 128 | $this->markTestSkipped(); 129 | } 130 | 131 | ini_set('session.serialize_handler', 'wddx'); 132 | $this->setExpectedException('RuntimeException'); 133 | new SessionProvider($this->getMockBuilder($this->getComponentClassString())->getMock(), $this->getMockBuilder('SessionHandlerInterface')->getMock()); 134 | } 135 | 136 | protected function doOpen($conn) { 137 | $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); 138 | $request->expects($this->any())->method('getHeader')->will($this->returnValue([])); 139 | 140 | $this->_serv->onOpen($conn, $request); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /tests/unit/Session/Storage/VirtualSessionStoragePDOTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('Session test requires PDO and pdo_sqlite'); 23 | } 24 | 25 | $schema = <<_pathToDB = tempnam(sys_get_temp_dir(), 'SQ3');; 34 | $dsn = 'sqlite:' . $this->_pathToDB; 35 | 36 | $pdo = new \PDO($dsn); 37 | $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); 38 | $pdo->exec($schema); 39 | $pdo = null; 40 | 41 | $sessionHandler = new PdoSessionHandler($dsn); 42 | $serializer = new PhpHandler(); 43 | $this->_virtualSessionStorage = new VirtualSessionStorage($sessionHandler, 'foobar', $serializer); 44 | $this->_virtualSessionStorage->registerBag(new FlashBag()); 45 | $this->_virtualSessionStorage->registerBag(new AttributeBag()); 46 | } 47 | 48 | /** 49 | * @after 50 | */ 51 | public function tearDownHandler() { 52 | unlink($this->_pathToDB); 53 | } 54 | 55 | public function testStartWithDSN() { 56 | $this->_virtualSessionStorage->start(); 57 | 58 | $this->assertTrue($this->_virtualSessionStorage->isStarted()); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/unit/Wamp/ServerProtocolTest.php: -------------------------------------------------------------------------------- 1 | _app = new TestComponent; 22 | $this->_comp = new ServerProtocol($this->_app); 23 | } 24 | 25 | protected function newConn() { 26 | return new Connection; 27 | } 28 | 29 | public function invalidMessageProvider() { 30 | return [ 31 | [0] 32 | , [3] 33 | , [4] 34 | , [8] 35 | , [9] 36 | ]; 37 | } 38 | 39 | /** 40 | * @dataProvider invalidMessageProvider 41 | */ 42 | public function testInvalidMessages($type) { 43 | if (method_exists($this, 'expectException')) { 44 | $this->expectException('Ratchet\Wamp\Exception'); 45 | } else { 46 | $this->setExpectedException('Ratchet\Wamp\Exception'); 47 | } 48 | 49 | $conn = $this->newConn(); 50 | $this->_comp->onOpen($conn); 51 | $this->_comp->onMessage($conn, json_encode([$type])); 52 | } 53 | 54 | public function testWelcomeMessage() { 55 | $conn = $this->newConn(); 56 | 57 | $this->_comp->onOpen($conn); 58 | 59 | $message = $conn->last['send']; 60 | $json = json_decode($message); 61 | 62 | $this->assertEquals(4, count($json)); 63 | $this->assertEquals(0, $json[0]); 64 | $this->assertTrue(is_string($json[1])); 65 | $this->assertEquals(1, $json[2]); 66 | } 67 | 68 | public function testSubscribe() { 69 | $uri = 'http://example.com'; 70 | $clientMessage = array(5, $uri); 71 | 72 | $conn = $this->newConn(); 73 | 74 | $this->_comp->onOpen($conn); 75 | $this->_comp->onMessage($conn, json_encode($clientMessage)); 76 | 77 | $this->assertEquals($uri, $this->_app->last['onSubscribe'][1]); 78 | } 79 | 80 | public function testUnSubscribe() { 81 | $uri = 'http://example.com/endpoint'; 82 | $clientMessage = array(6, $uri); 83 | 84 | $conn = $this->newConn(); 85 | 86 | $this->_comp->onOpen($conn); 87 | $this->_comp->onMessage($conn, json_encode($clientMessage)); 88 | 89 | $this->assertEquals($uri, $this->_app->last['onUnSubscribe'][1]); 90 | } 91 | 92 | public function callProvider() { 93 | return [ 94 | [2, 'a', 'b'] 95 | , [2, ['a', 'b']] 96 | , [1, 'one'] 97 | , [3, 'one', 'two', 'three'] 98 | , [3, ['un', 'deux', 'trois']] 99 | , [2, 'hi', ['hello', 'world']] 100 | , [2, ['hello', 'world'], 'hi'] 101 | , [2, ['hello' => 'world', 'herp' => 'derp']] 102 | ]; 103 | } 104 | 105 | /** 106 | * @dataProvider callProvider 107 | */ 108 | public function testCall() { 109 | $args = func_get_args(); 110 | $paramNum = array_shift($args); 111 | 112 | $uri = 'http://example.com/endpoint/' . rand(1, 100); 113 | $id = uniqid('', false); 114 | $clientMessage = array_merge(array(2, $id, $uri), $args); 115 | 116 | $conn = $this->newConn(); 117 | 118 | $this->_comp->onOpen($conn); 119 | $this->_comp->onMessage($conn, json_encode($clientMessage)); 120 | 121 | $this->assertEquals($id, $this->_app->last['onCall'][1]); 122 | $this->assertEquals($uri, $this->_app->last['onCall'][2]); 123 | 124 | $this->assertEquals($paramNum, count($this->_app->last['onCall'][3])); 125 | } 126 | 127 | public function testPublish() { 128 | $conn = $this->newConn(); 129 | 130 | $topic = 'pubsubhubbub'; 131 | $event = 'Here I am, publishing data'; 132 | 133 | $clientMessage = array(7, $topic, $event); 134 | 135 | $this->_comp->onOpen($conn); 136 | $this->_comp->onMessage($conn, json_encode($clientMessage)); 137 | 138 | $this->assertEquals($topic, $this->_app->last['onPublish'][1]); 139 | $this->assertEquals($event, $this->_app->last['onPublish'][2]); 140 | $this->assertEquals(array(), $this->_app->last['onPublish'][3]); 141 | $this->assertEquals(array(), $this->_app->last['onPublish'][4]); 142 | } 143 | 144 | public function testPublishAndExcludeMe() { 145 | $conn = $this->newConn(); 146 | 147 | $this->_comp->onOpen($conn); 148 | $this->_comp->onMessage($conn, json_encode(array(7, 'topic', 'event', true))); 149 | 150 | $this->assertEquals($conn->WAMP->sessionId, $this->_app->last['onPublish'][3][0]); 151 | } 152 | 153 | public function testPublishAndEligible() { 154 | $conn = $this->newConn(); 155 | 156 | $buddy = uniqid('', false); 157 | $friend = uniqid('', false); 158 | 159 | $this->_comp->onOpen($conn); 160 | $this->_comp->onMessage($conn, json_encode(array(7, 'topic', 'event', false, array($buddy, $friend)))); 161 | 162 | $this->assertEquals(array(), $this->_app->last['onPublish'][3]); 163 | $this->assertEquals(2, count($this->_app->last['onPublish'][4])); 164 | } 165 | 166 | public function eventProvider() { 167 | return array( 168 | array('http://example.com', array('one', 'two')) 169 | , array('curie', array(array('hello' => 'world', 'herp' => 'derp'))) 170 | ); 171 | } 172 | 173 | /** 174 | * @dataProvider eventProvider 175 | */ 176 | public function testEvent($topic, $payload) { 177 | $conn = new WampConnection($this->newConn()); 178 | $conn->event($topic, $payload); 179 | 180 | $eventString = $conn->last['send']; 181 | 182 | $this->assertSame(array(8, $topic, $payload), json_decode($eventString, true)); 183 | } 184 | 185 | public function testOnClosePropagation() { 186 | $conn = new Connection; 187 | 188 | $this->_comp->onOpen($conn); 189 | $this->_comp->onClose($conn); 190 | 191 | $class = new \ReflectionClass('Ratchet\\Wamp\\WampConnection'); 192 | $method = $class->getMethod('getConnection'); 193 | $method->setAccessible(true); 194 | 195 | $check = $method->invokeArgs($this->_app->last['onClose'][0], array()); 196 | 197 | $this->assertSame($conn, $check); 198 | } 199 | 200 | public function testOnErrorPropagation() { 201 | $conn = new Connection; 202 | 203 | $e = new \Exception('Nope'); 204 | 205 | $this->_comp->onOpen($conn); 206 | $this->_comp->onError($conn, $e); 207 | 208 | $class = new \ReflectionClass('Ratchet\\Wamp\\WampConnection'); 209 | $method = $class->getMethod('getConnection'); 210 | $method->setAccessible(true); 211 | 212 | $check = $method->invokeArgs($this->_app->last['onError'][0], array()); 213 | 214 | $this->assertSame($conn, $check); 215 | $this->assertSame($e, $this->_app->last['onError'][1]); 216 | } 217 | 218 | public function testPrefix() { 219 | $conn = new WampConnection($this->newConn()); 220 | $this->_comp->onOpen($conn); 221 | 222 | $prefix = 'incoming'; 223 | $fullURI = "http://example.com/$prefix"; 224 | $method = 'call'; 225 | 226 | $this->_comp->onMessage($conn, json_encode(array(1, $prefix, $fullURI))); 227 | 228 | $this->assertEquals($fullURI, $conn->WAMP->prefixes[$prefix]); 229 | $this->assertEquals("$fullURI#$method", $conn->getUri("$prefix:$method")); 230 | } 231 | 232 | public function testMessageMustBeJson() { 233 | if (method_exists($this, 'expectException')) { 234 | $this->expectException('Ratchet\Wamp\Exception'); 235 | } else { 236 | $this->setExpectedException('Ratchet\Wamp\Exception'); 237 | } 238 | 239 | $conn = new Connection; 240 | 241 | $this->_comp->onOpen($conn); 242 | $this->_comp->onMessage($conn, 'Hello World!'); 243 | } 244 | 245 | public function testGetSubProtocolsReturnsArray() { 246 | $this->assertTrue(is_array($this->_comp->getSubProtocols())); 247 | } 248 | 249 | public function testGetSubProtocolsGetFromApp() { 250 | $this->_app->protocols = array('hello', 'world'); 251 | 252 | $this->assertGreaterThanOrEqual(3, count($this->_comp->getSubProtocols())); 253 | } 254 | 255 | public function testWampOnMessageApp() { 256 | $app = $this->getMockBuilder('Ratchet\\Wamp\\WampServerInterface')->getMock(); 257 | $wamp = new ServerProtocol($app); 258 | 259 | $this->assertContains('wamp', $wamp->getSubProtocols()); 260 | } 261 | 262 | public function badFormatProvider() { 263 | return array( 264 | array(json_encode(true)) 265 | , array('{"valid":"json", "invalid": "message"}') 266 | , array('{"0": "fail", "hello": "world"}') 267 | ); 268 | } 269 | 270 | /** 271 | * @dataProvider badFormatProvider 272 | */ 273 | public function testValidJsonButInvalidProtocol($message) { 274 | if (method_exists($this, 'expectException')) { 275 | $this->expectException('Ratchet\Wamp\Exception'); 276 | } else { 277 | $this->setExpectedException('Ratchet\Wamp\Exception'); 278 | } 279 | 280 | $conn = $this->newConn(); 281 | $this->_comp->onOpen($conn); 282 | $this->_comp->onMessage($conn, $message); 283 | } 284 | 285 | public function testBadClientInputFromNonStringTopic() { 286 | if (method_exists($this, 'expectException')) { 287 | $this->expectException('Ratchet\Wamp\Exception'); 288 | } else { 289 | $this->setExpectedException('Ratchet\Wamp\Exception'); 290 | } 291 | 292 | $conn = new WampConnection($this->newConn()); 293 | $this->_comp->onOpen($conn); 294 | 295 | $this->_comp->onMessage($conn, json_encode([5, ['hells', 'nope']])); 296 | } 297 | 298 | public function testBadPrefixWithNonStringTopic() { 299 | if (method_exists($this, 'expectException')) { 300 | $this->expectException('Ratchet\Wamp\Exception'); 301 | } else { 302 | $this->setExpectedException('Ratchet\Wamp\Exception'); 303 | } 304 | 305 | $conn = new WampConnection($this->newConn()); 306 | $this->_comp->onOpen($conn); 307 | 308 | $this->_comp->onMessage($conn, json_encode([1, ['hells', 'nope'], ['bad', 'input']])); 309 | } 310 | 311 | public function testBadPublishWithNonStringTopic() { 312 | if (method_exists($this, 'expectException')) { 313 | $this->expectException('Ratchet\Wamp\Exception'); 314 | } else { 315 | $this->setExpectedException('Ratchet\Wamp\Exception'); 316 | } 317 | 318 | $conn = new WampConnection($this->newConn()); 319 | $this->_comp->onOpen($conn); 320 | 321 | $this->_comp->onMessage($conn, json_encode([7, ['bad', 'input'], 'Hider'])); 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /tests/unit/Wamp/TopicManagerTest.php: -------------------------------------------------------------------------------- 1 | conn = $this->getMockBuilder('Ratchet\Mock\Connection')->getMock(); 27 | $this->mock = $this->getMockBuilder('Ratchet\Wamp\WampServerInterface')->getMock(); 28 | $this->mngr = new TopicManager($this->mock); 29 | 30 | $this->conn->WAMP = new \StdClass; 31 | $this->mngr->onOpen($this->conn); 32 | } 33 | 34 | public function testGetTopicReturnsTopicObject() { 35 | $class = new \ReflectionClass('Ratchet\Wamp\TopicManager'); 36 | $method = $class->getMethod('getTopic'); 37 | $method->setAccessible(true); 38 | 39 | $topic = $method->invokeArgs($this->mngr, array('The Topic')); 40 | 41 | $this->assertInstanceOf('Ratchet\Wamp\Topic', $topic); 42 | } 43 | 44 | public function testGetTopicCreatesTopicWithSameName() { 45 | $name = 'The Topic'; 46 | 47 | $class = new \ReflectionClass('Ratchet\Wamp\TopicManager'); 48 | $method = $class->getMethod('getTopic'); 49 | $method->setAccessible(true); 50 | 51 | $topic = $method->invokeArgs($this->mngr, array($name)); 52 | 53 | $this->assertEquals($name, $topic->getId()); 54 | } 55 | 56 | public function testGetTopicReturnsSameObject() { 57 | $class = new \ReflectionClass('Ratchet\Wamp\TopicManager'); 58 | $method = $class->getMethod('getTopic'); 59 | $method->setAccessible(true); 60 | 61 | $topic = $method->invokeArgs($this->mngr, array('No copy')); 62 | $again = $method->invokeArgs($this->mngr, array('No copy')); 63 | 64 | $this->assertSame($topic, $again); 65 | } 66 | 67 | public function testOnOpen() { 68 | $this->mock->expects($this->once())->method('onOpen'); 69 | $this->mngr->onOpen($this->conn); 70 | } 71 | 72 | public function testOnCall() { 73 | $id = uniqid(); 74 | 75 | $this->mock->expects($this->once())->method('onCall')->with( 76 | $this->conn 77 | , $id 78 | , $this->isInstanceOf('Ratchet\Wamp\Topic') 79 | , array() 80 | ); 81 | 82 | $this->mngr->onCall($this->conn, $id, 'new topic', array()); 83 | } 84 | 85 | public function testOnSubscribeCreatesTopicObject() { 86 | $this->mock->expects($this->once())->method('onSubscribe')->with( 87 | $this->conn, $this->isInstanceOf('Ratchet\Wamp\Topic') 88 | ); 89 | 90 | $this->mngr->onSubscribe($this->conn, 'new topic'); 91 | } 92 | 93 | public function testTopicIsInConnectionOnSubscribe() { 94 | $name = 'New Topic'; 95 | 96 | $class = new \ReflectionClass('Ratchet\Wamp\TopicManager'); 97 | $method = $class->getMethod('getTopic'); 98 | $method->setAccessible(true); 99 | 100 | $topic = $method->invokeArgs($this->mngr, array($name)); 101 | 102 | $this->mngr->onSubscribe($this->conn, $name); 103 | 104 | $this->assertTrue($this->conn->WAMP->subscriptions->contains($topic)); 105 | } 106 | 107 | public function testDoubleSubscriptionFiresOnce() { 108 | $this->mock->expects($this->exactly(1))->method('onSubscribe'); 109 | 110 | $this->mngr->onSubscribe($this->conn, 'same topic'); 111 | $this->mngr->onSubscribe($this->conn, 'same topic'); 112 | } 113 | 114 | public function testUnsubscribeEvent() { 115 | $name = 'in and out'; 116 | $this->mock->expects($this->once())->method('onUnsubscribe')->with( 117 | $this->conn, $this->isInstanceOf('Ratchet\Wamp\Topic') 118 | ); 119 | 120 | $this->mngr->onSubscribe($this->conn, $name); 121 | $this->mngr->onUnsubscribe($this->conn, $name); 122 | } 123 | 124 | public function testUnsubscribeFiresOnce() { 125 | $name = 'getting sleepy'; 126 | $this->mock->expects($this->exactly(1))->method('onUnsubscribe'); 127 | 128 | $this->mngr->onSubscribe($this->conn, $name); 129 | $this->mngr->onUnsubscribe($this->conn, $name); 130 | $this->mngr->onUnsubscribe($this->conn, $name); 131 | } 132 | 133 | public function testUnsubscribeRemovesTopicFromConnection() { 134 | $name = 'Bye Bye Topic'; 135 | 136 | $class = new \ReflectionClass('Ratchet\Wamp\TopicManager'); 137 | $method = $class->getMethod('getTopic'); 138 | $method->setAccessible(true); 139 | 140 | $topic = $method->invokeArgs($this->mngr, array($name)); 141 | 142 | $this->mngr->onSubscribe($this->conn, $name); 143 | $this->mngr->onUnsubscribe($this->conn, $name); 144 | 145 | $this->assertFalse($this->conn->WAMP->subscriptions->contains($topic)); 146 | } 147 | 148 | public function testOnPublishBubbles() { 149 | $msg = 'Cover all the code!'; 150 | 151 | $this->mock->expects($this->once())->method('onPublish')->with( 152 | $this->conn 153 | , $this->isInstanceOf('Ratchet\Wamp\Topic') 154 | , $msg 155 | , $this->isType('array') 156 | , $this->isType('array') 157 | ); 158 | 159 | $this->mngr->onPublish($this->conn, 'topic coverage', $msg, array(), array()); 160 | } 161 | 162 | public function testOnCloseBubbles() { 163 | $this->mock->expects($this->once())->method('onClose')->with($this->conn); 164 | $this->mngr->onClose($this->conn); 165 | } 166 | 167 | protected function topicProvider($name) { 168 | $class = new \ReflectionClass('Ratchet\Wamp\TopicManager'); 169 | $method = $class->getMethod('getTopic'); 170 | $method->setAccessible(true); 171 | 172 | $attribute = $class->getProperty('topicLookup'); 173 | $attribute->setAccessible(true); 174 | 175 | $topic = $method->invokeArgs($this->mngr, array($name)); 176 | 177 | return array($topic, $attribute); 178 | } 179 | 180 | public function testConnIsRemovedFromTopicOnClose() { 181 | $name = 'State Testing'; 182 | list($topic, $attribute) = $this->topicProvider($name); 183 | 184 | $this->assertCount(1, $attribute->getValue($this->mngr)); 185 | 186 | $this->mngr->onSubscribe($this->conn, $name); 187 | $this->mngr->onClose($this->conn); 188 | 189 | $this->assertFalse($topic->has($this->conn)); 190 | } 191 | 192 | public static function topicConnExpectationProvider() { 193 | return [ 194 | [ 'onClose', 0] 195 | , ['onUnsubscribe', 0] 196 | ]; 197 | } 198 | 199 | /** 200 | * @dataProvider topicConnExpectationProvider 201 | */ 202 | public function testTopicRetentionFromLeavingConnections($methodCall, $expectation) { 203 | $topicName = 'checkTopic'; 204 | list($topic, $attribute) = $this->topicProvider($topicName); 205 | 206 | $this->mngr->onSubscribe($this->conn, $topicName); 207 | call_user_func_array(array($this->mngr, $methodCall), array($this->conn, $topicName)); 208 | 209 | $this->assertCount($expectation, $attribute->getValue($this->mngr)); 210 | } 211 | 212 | public function testOnErrorBubbles() { 213 | $e = new \Exception('All work and no play makes Chris a dull boy'); 214 | $this->mock->expects($this->once())->method('onError')->with($this->conn, $e); 215 | 216 | $this->mngr->onError($this->conn, $e); 217 | } 218 | 219 | public function testGetSubProtocolsReturnsArray() { 220 | if (method_exists($this, 'assertIsArray')) { 221 | // PHPUnit 7+ 222 | $this->assertIsArray($this->mngr->getSubProtocols()); 223 | } else { 224 | // legacy PHPUnit 225 | $this->assertInternalType('array', $this->mngr->getSubProtocols()); 226 | } 227 | } 228 | 229 | public function testGetSubProtocolsBubbles() { 230 | $subs = array('hello', 'world'); 231 | $app = $this->getMockBuilder('Ratchet\Wamp\Stub\WsWampServerInterface')->getMock(); 232 | $app->expects($this->once())->method('getSubProtocols')->will($this->returnValue($subs)); 233 | $mngr = new TopicManager($app); 234 | 235 | $this->assertEquals($subs, $mngr->getSubProtocols()); 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /tests/unit/Wamp/TopicTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($id, $topic->getId()); 15 | } 16 | 17 | public function testAddAndCount() { 18 | $topic = new Topic('merp'); 19 | 20 | $topic->add($this->newConn()); 21 | $topic->add($this->newConn()); 22 | $topic->add($this->newConn()); 23 | 24 | $this->assertEquals(3, count($topic)); 25 | } 26 | 27 | public function testRemove() { 28 | $topic = new Topic('boop'); 29 | $tracked = $this->newConn(); 30 | 31 | $topic->add($this->newConn()); 32 | $topic->add($tracked); 33 | $topic->add($this->newConn()); 34 | 35 | $topic->remove($tracked); 36 | 37 | $this->assertEquals(2, count($topic)); 38 | } 39 | 40 | public function testBroadcast() { 41 | $msg = 'Hello World!'; 42 | $name = 'Batman'; 43 | $protocol = json_encode(array(8, $name, $msg)); 44 | 45 | $first = $this->getMockBuilder('Ratchet\\Wamp\\WampConnection')->setMethods(array('send'))->setConstructorArgs(array($this->getMockBuilder('Ratchet\Mock\Connection')->getMock()))->getMock(); 46 | $second = $this->getMockBuilder('Ratchet\\Wamp\\WampConnection')->setMethods(array('send'))->setConstructorArgs(array($this->getMockBuilder('Ratchet\Mock\Connection')->getMock()))->getMock(); 47 | 48 | $first->expects($this->once()) 49 | ->method('send') 50 | ->with($this->equalTo($protocol)); 51 | 52 | $second->expects($this->once()) 53 | ->method('send') 54 | ->with($this->equalTo($protocol)); 55 | 56 | $topic = new Topic($name); 57 | $topic->add($first); 58 | $topic->add($second); 59 | 60 | $topic->broadcast($msg); 61 | } 62 | 63 | public function testBroadcastWithExclude() { 64 | $msg = 'Hello odd numbers'; 65 | $name = 'Excluding'; 66 | $protocol = json_encode(array(8, $name, $msg)); 67 | 68 | $first = $this->getMockBuilder('Ratchet\\Wamp\\WampConnection')->setMethods(array('send'))->setConstructorArgs(array($this->getMockBuilder('Ratchet\Mock\Connection')->getMock()))->getMock(); 69 | $second = $this->getMockBuilder('Ratchet\\Wamp\\WampConnection')->setMethods(array('send'))->setConstructorArgs(array($this->getMockBuilder('Ratchet\Mock\Connection')->getMock()))->getMock(); 70 | $third = $this->getMockBuilder('Ratchet\\Wamp\\WampConnection')->setMethods(array('send'))->setConstructorArgs(array($this->getMockBuilder('Ratchet\Mock\Connection')->getMock()))->getMock(); 71 | 72 | $first->expects($this->once()) 73 | ->method('send') 74 | ->with($this->equalTo($protocol)); 75 | 76 | $second->expects($this->never())->method('send'); 77 | 78 | $third->expects($this->once()) 79 | ->method('send') 80 | ->with($this->equalTo($protocol)); 81 | 82 | $topic = new Topic($name); 83 | $topic->add($first); 84 | $topic->add($second); 85 | $topic->add($third); 86 | 87 | $topic->broadcast($msg, array($second->WAMP->sessionId)); 88 | } 89 | 90 | public function testBroadcastWithEligible() { 91 | $msg = 'Hello white list'; 92 | $name = 'Eligible'; 93 | $protocol = json_encode(array(8, $name, $msg)); 94 | 95 | $first = $this->getMockBuilder('Ratchet\\Wamp\\WampConnection')->setMethods(array('send'))->setConstructorArgs(array($this->getMockBuilder('Ratchet\Mock\Connection')->getMock()))->getMock(); 96 | $second = $this->getMockBuilder('Ratchet\\Wamp\\WampConnection')->setMethods(array('send'))->setConstructorArgs(array($this->getMockBuilder('Ratchet\Mock\Connection')->getMock()))->getMock(); 97 | $third = $this->getMockBuilder('Ratchet\\Wamp\\WampConnection')->setMethods(array('send'))->setConstructorArgs(array($this->getMockBuilder('Ratchet\Mock\Connection')->getMock()))->getMock(); 98 | 99 | $first->expects($this->once()) 100 | ->method('send') 101 | ->with($this->equalTo($protocol)); 102 | 103 | $second->expects($this->never())->method('send'); 104 | 105 | $third->expects($this->once()) 106 | ->method('send') 107 | ->with($this->equalTo($protocol)); 108 | 109 | $topic = new Topic($name); 110 | $topic->add($first); 111 | $topic->add($second); 112 | $topic->add($third); 113 | 114 | $topic->broadcast($msg, array(), array($first->WAMP->sessionId, $third->WAMP->sessionId)); 115 | } 116 | 117 | public function testIterator() { 118 | $first = $this->newConn(); 119 | $second = $this->newConn(); 120 | $third = $this->newConn(); 121 | 122 | $topic = new Topic('Joker'); 123 | $topic->add($first)->add($second)->add($third); 124 | 125 | $check = array($first, $second, $third); 126 | 127 | foreach ($topic as $mock) { 128 | $this->assertNotSame(false, array_search($mock, $check)); 129 | } 130 | } 131 | 132 | public function testToString() { 133 | $name = 'Bane'; 134 | $topic = new Topic($name); 135 | 136 | $this->assertEquals($name, (string)$topic); 137 | } 138 | 139 | public function testDoesHave() { 140 | $conn = $this->newConn(); 141 | $topic = new Topic('Two Face'); 142 | $topic->add($conn); 143 | 144 | $this->assertTrue($topic->has($conn)); 145 | } 146 | 147 | public function testDoesNotHave() { 148 | $conn = $this->newConn(); 149 | $topic = new Topic('Alfred'); 150 | 151 | $this->assertFalse($topic->has($conn)); 152 | } 153 | 154 | public function testDoesNotHaveAfterRemove() { 155 | $conn = $this->newConn(); 156 | $topic = new Topic('Ras'); 157 | 158 | $topic->add($conn)->remove($conn); 159 | 160 | $this->assertFalse($topic->has($conn)); 161 | } 162 | 163 | protected function newConn() { 164 | return new WampConnection($this->getMockBuilder('Ratchet\Mock\Connection')->getMock()); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /tests/unit/Wamp/WampConnectionTest.php: -------------------------------------------------------------------------------- 1 | mock = $this->getMockBuilder('Ratchet\Mock\Connection')->getMock(); 18 | $this->conn = new WampConnection($this->mock); 19 | } 20 | 21 | public function testCallResult() { 22 | $callId = uniqid(); 23 | $data = array('hello' => 'world', 'herp' => 'derp'); 24 | 25 | $this->mock->expects($this->once())->method('send')->with(json_encode(array(3, $callId, $data))); 26 | 27 | $this->conn->callResult($callId, $data); 28 | } 29 | 30 | public function testCallError() { 31 | $callId = uniqid(); 32 | $uri = 'http://example.com/end/point'; 33 | 34 | $this->mock->expects($this->once())->method('send')->with(json_encode(array(4, $callId, $uri, ''))); 35 | 36 | $this->conn->callError($callId, $uri); 37 | } 38 | 39 | public function testCallErrorWithTopic() { 40 | $callId = uniqid(); 41 | $uri = 'http://example.com/end/point'; 42 | 43 | $this->mock->expects($this->once())->method('send')->with(json_encode(array(4, $callId, $uri, ''))); 44 | 45 | $this->conn->callError($callId, new Topic($uri)); 46 | } 47 | 48 | public function testDetailedCallError() { 49 | $callId = uniqid(); 50 | $uri = 'http://example.com/end/point'; 51 | $desc = 'beep boop beep'; 52 | $detail = 'Error: Too much awesome'; 53 | 54 | $this->mock->expects($this->once())->method('send')->with(json_encode(array(4, $callId, $uri, $desc, $detail))); 55 | 56 | $this->conn->callError($callId, $uri, $desc, $detail); 57 | } 58 | 59 | public function testPrefix() { 60 | $shortOut = 'outgoing'; 61 | $longOut = 'http://example.com/outgoing'; 62 | 63 | $this->mock->expects($this->once())->method('send')->with(json_encode(array(1, $shortOut, $longOut))); 64 | 65 | $this->conn->prefix($shortOut, $longOut); 66 | } 67 | 68 | public function testGetUriWhenNoCurieGiven() { 69 | $uri = 'http://example.com/noshort'; 70 | 71 | $this->assertEquals($uri, $this->conn->getUri($uri)); 72 | } 73 | 74 | public function testClose() { 75 | $mock = $this->getMockBuilder('Ratchet\Mock\Connection')->getMock(); 76 | $conn = new WampConnection($mock); 77 | 78 | $mock->expects($this->once())->method('close'); 79 | 80 | $conn->close(); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /tests/unit/Wamp/WampServerTest.php: -------------------------------------------------------------------------------- 1 | _app->expects($this->once())->method('onPublish')->with( 25 | $this->isExpectedConnection() 26 | , $this->isInstanceOf('Ratchet\Wamp\Topic') 27 | , $published 28 | , array() 29 | , array() 30 | ); 31 | 32 | $this->_serv->onMessage($this->_conn, json_encode(array(7, 'topic', $published))); 33 | } 34 | 35 | public function testGetSubProtocols() { 36 | // todo: could expand on this 37 | if (method_exists($this, 'assertIsArray')) { 38 | // PHPUnit 7+ 39 | $this->assertIsArray($this->_serv->getSubProtocols()); 40 | } else { 41 | // legacy PHPUnit 42 | $this->assertInternalType('array', $this->_serv->getSubProtocols()); 43 | } 44 | } 45 | 46 | public function testConnectionClosesOnInvalidJson() { 47 | $this->_conn->expects($this->once())->method('close'); 48 | $this->_serv->onMessage($this->_conn, 'invalid json'); 49 | } 50 | 51 | public function testConnectionClosesOnProtocolError() { 52 | $this->_conn->expects($this->once())->method('close'); 53 | $this->_serv->onMessage($this->_conn, json_encode(array('valid' => 'json', 'invalid' => 'protocol'))); 54 | } 55 | } 56 | --------------------------------------------------------------------------------