pax_global_header00006660000000000000000000000064134171140400014505gustar00rootroot0000000000000052 comment=3a7d5b78e0deaec82f42513a4a3193a8eb12feb1 Pawl-0.3.4/000077500000000000000000000000001341711404000124145ustar00rootroot00000000000000Pawl-0.3.4/.gitignore000066400000000000000000000000511341711404000144000ustar00rootroot00000000000000phpunit.xml reports vendor composer.lock Pawl-0.3.4/.travis.yml000066400000000000000000000005471341711404000145330ustar00rootroot00000000000000language: php php: - 5.4 - 5.5 - 5.6 - 7.0 - 7.1 - 7.2 - 7.3 - hhvm dist: trusty matrix: allow_failures: - php: hhvm before_script: - sh -c 'if [ "$TRAVIS_PHP_VERSION" != "hhvm" ]; then echo "session.serialize_handler = php" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini; fi;' - composer install --dev --prefer-source Pawl-0.3.4/LICENSE000066400000000000000000000020371341711404000134230ustar00rootroot00000000000000Copyright (c) 2015 Chris Boden Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Pawl-0.3.4/README.md000066400000000000000000000066021341711404000136770ustar00rootroot00000000000000# Pawl [![Autobahn Testsuite](https://img.shields.io/badge/Autobahn-passing-brightgreen.svg)](http://socketo.me/reports/pawl/index.html) [![Build Status](https://travis-ci.org/ratchetphp/Pawl.svg?branch=master)](https://travis-ci.org/ratchetphp/Pawl) An asynchronous WebSocket client in PHP #### Install via composer: composer require ratchet/pawl #### Usage Pawl as a standalone app: Connect to an echo server, send a message, display output, close connection: ```php then(function($conn) { $conn->on('message', function($msg) use ($conn) { echo "Received: {$msg}\n"; $conn->close(); }); $conn->send('Hello World!'); }, function ($e) { echo "Could not connect: {$e->getMessage()}\n"; }); ``` --- #### Classes There are 3 primary classes to be aware of and use in Pawl: ##### Connector: Makes HTTP requests to servers returning a promise that, if successful, will resolve to a WebSocket object. A connector is configured via its constructor and a request is made by invoking the class. Multiple connections can be established through a single connector. The invoke mehtod has 3 parameters: * **$url**: String; A valid uri string (starting with ws:// or wss://) to connect to (also accepts PSR-7 Uri object) * **$subProtocols**: Array; An optional indexed array of WebSocket sub-protocols to negotiate to the server with. The connection will fail if the client and server can not agree on one if any are provided * **$headers**: Array; An optional associative array of additional headers requests to use when initiating the handshake. A common header to set is `Origin` ##### WebSocket: This is the object used to interact with a WebSocket server. It has two methods: `send` and `close`. It has two public properties: `request` and `response` which are PSR-7 objects representing the client and server side HTTP handshake headers used to establish the WebSocket connection. ##### Message: This is the object received from a WebSocket server. It has a `__toString` method which is how most times you will want to access the data received. If you need to do binary messaging you will most likely need to use methods on the object. #### Example A more in-depth example using explicit interfaces: Requesting sub-protocols, and sending custom headers while using a specific React Event Loop: ```php '8.8.8.8', 'timeout' => 10 ]); $connector = new \Ratchet\Client\Connector($loop, $reactConnector); $connector('ws://127.0.0.1:9000', ['protocol1', 'subprotocol2'], ['Origin' => 'http://localhost']) ->then(function(Ratchet\Client\WebSocket $conn) { $conn->on('message', function(\Ratchet\RFC6455\Messaging\MessageInterface $msg) use ($conn) { echo "Received: {$msg}\n"; $conn->close(); }); $conn->on('close', function($code = null, $reason = null) { echo "Connection closed ({$code} - {$reason})\n"; }); $conn->send('Hello World!'); }, function(\Exception $e) use ($loop) { echo "Could not connect: {$e->getMessage()}\n"; $loop->stop(); }); $loop->run(); ``` Pawl-0.3.4/composer.json000066400000000000000000000011471341711404000151410ustar00rootroot00000000000000{ "name": "ratchet/pawl" , "description": "Asynchronous WebSocket client" , "keywords": ["WebSocket", "client", "Ratchet", "async", "websocket client"] , "license": "MIT" , "autoload": { "psr-4": { "Ratchet\\Client\\": "src" } , "files": ["src/functions_include.php"] } , "require": { "php": ">=5.4" , "react/socket": "^1.0 || ^0.8 || ^0.7" , "evenement/evenement": "^3.0 || ^2.0" , "ratchet/rfc6455": "^0.2.3" } , "require-dev": { "phpunit/phpunit": "~4.8" } , "suggest": { "reactivex/rxphp": "~2.0" } } Pawl-0.3.4/phpunit.xml.dist000066400000000000000000000011071341711404000155660ustar00rootroot00000000000000 ./tests/unit/ ./src/ Pawl-0.3.4/src/000077500000000000000000000000001341711404000132035ustar00rootroot00000000000000Pawl-0.3.4/src/Connector.php000066400000000000000000000126211341711404000156500ustar00rootroot00000000000000 20 ]); } $this->_loop = $loop; $this->_connector = $connector; $this->_negotiator = new ClientNegotiator; } /** * @param string $url * @param array $subProtocols * @param array $headers * @return \React\Promise\PromiseInterface */ public function __invoke($url, array $subProtocols = [], array $headers = []) { try { $request = $this->generateRequest($url, $subProtocols, $headers); $uri = $request->getUri(); } catch (\Exception $e) { return new RejectedPromise($e); } $secure = 'wss' === substr($url, 0, 3); $connector = $this->_connector; $port = $uri->getPort() ?: ($secure ? 443 : 80); $scheme = $secure ? 'tls' : 'tcp'; $uriString = $scheme . '://' . $uri->getHost() . ':' . $port; $connecting = $connector->connect($uriString); $futureWsConn = new Deferred(function ($_, $reject) use ($url, $connecting) { $reject(new \RuntimeException( 'Connection to ' . $url . ' cancelled during handshake' )); // either close active connection or cancel pending connection attempt $connecting->then(function (ConnectionInterface $connection) { $connection->close(); }); $connecting->cancel(); }); $connecting->then(function(ConnectionInterface $conn) use ($request, $subProtocols, $futureWsConn) { $earlyClose = function() use ($futureWsConn) { $futureWsConn->reject(new \RuntimeException('Connection closed before handshake')); }; $stream = $conn; $stream->on('close', $earlyClose); $futureWsConn->promise()->then(function() use ($stream, $earlyClose) { $stream->removeListener('close', $earlyClose); }); $buffer = ''; $headerParser = function($data) use ($stream, &$headerParser, &$buffer, $futureWsConn, $request, $subProtocols) { $buffer .= $data; if (false == strpos($buffer, "\r\n\r\n")) { return; } $stream->removeListener('data', $headerParser); $response = gPsr\parse_response($buffer); if (!$this->_negotiator->validateResponse($request, $response)) { $futureWsConn->reject(new \DomainException(gPsr\str($response))); $stream->close(); return; } $acceptedProtocol = $response->getHeader('Sec-WebSocket-Protocol'); if ((count($subProtocols) > 0) && 1 !== count(array_intersect($subProtocols, $acceptedProtocol))) { $futureWsConn->reject(new \DomainException('Server did not respond with an expected Sec-WebSocket-Protocol')); $stream->close(); return; } $futureWsConn->resolve(new WebSocket($stream, $response, $request)); $futureWsConn->promise()->then(function(WebSocket $conn) use ($stream) { $stream->emit('data', [$conn->response->getBody(), $stream]); }); }; $stream->on('data', $headerParser); $stream->write(gPsr\str($request)); }, array($futureWsConn, 'reject')); return $futureWsConn->promise(); } /** * @param string $url * @param array $subProtocols * @param array $headers * @throws \InvalidArgumentException * @return \Psr\Http\Message\RequestInterface */ protected function generateRequest($url, array $subProtocols, array $headers) { $uri = gPsr\uri_for($url); $scheme = $uri->getScheme(); if (!in_array($scheme, ['ws', 'wss'])) { throw new \InvalidArgumentException(sprintf('Cannot connect to invalid URL (%s)', $url)); } $uri = $uri->withScheme('wss' === $scheme ? 'HTTPS' : 'HTTP'); $headers += ['User-Agent' => 'Ratchet-Pawl/0.3']; $request = array_reduce(array_keys($headers), function(RequestInterface $request, $header) use ($headers) { return $request->withHeader($header, $headers[$header]); }, $this->_negotiator->generateRequest($uri)); if (!$request->getHeader('Origin')) { $request = $request->withHeader('Origin', str_replace('ws', 'http', $scheme) . '://' . $uri->getHost()); } if (count($subProtocols) > 0) { $protocols = implode(',', $subProtocols); if ($protocols != "") { $request = $request->withHeader('Sec-WebSocket-Protocol', $protocols); } } return $request; } } Pawl-0.3.4/src/WebSocket.php000077500000000000000000000106601341711404000156100ustar00rootroot00000000000000_stream = $stream; $this->response = $response; $this->request = $request; $self = $this; $this->_close = function($code = null, $reason = null) use ($self) { static $sent = false; if ($sent) { return; } $sent = true; $self->emit('close', [$code, $reason, $self]); }; $reusableUAException = new \UnderflowException; $streamer = new MessageBuffer( new CloseFrameChecker, function(MessageInterface $msg) { $this->emit('message', [$msg, $this]); }, function(FrameInterface $frame) use (&$streamer) { switch ($frame->getOpcode()) { case Frame::OP_CLOSE: $frameContents = $frame->getPayload(); $reason = ''; $code = unpack('n', substr($frameContents, 0, 2)); $code = reset($code); if (($frameLen = strlen($frameContents)) > 2) { $reason = substr($frameContents, 2, $frameLen); } $closeFn = $this->_close; $closeFn($code, $reason); return $this->_stream->end($streamer->newFrame($frame->getPayload(), true, Frame::OP_CLOSE)->maskPayload()->getContents()); case Frame::OP_PING: $this->emit('ping', [$frame, $this]); return $this->send($streamer->newFrame($frame->getPayload(), true, Frame::OP_PONG)); case Frame::OP_PONG: return $this->emit('pong', [$frame, $this]); default: return $this->close(Frame::CLOSE_PROTOCOL); } }, false, function() use ($reusableUAException) { return $reusableUAException; } ); $stream->on('data', [$streamer, 'onData']); $stream->on('close', function () { $close = $this->_close; $close(Frame::CLOSE_ABNORMAL, 'Underlying connection closed'); }); $stream->on('error', function($error) { $this->emit('error', [$error, $this]); }); } public function send($msg) { if ($msg instanceof MessageInterface) { foreach ($msg as $frame) { $frame->maskPayload(); } } else { if (!($msg instanceof Frame)) { $msg = new Frame($msg); } $msg->maskPayload(); } $this->_stream->write($msg->getContents()); } public function close($code = 1000, $reason = '') { $frame = new Frame(pack('n', $code) . $reason, true, Frame::OP_CLOSE); $frame->maskPayload(); $this->_stream->write($frame->getContents()); $closeFn = $this->_close; $closeFn($code, $reason); $this->_stream->end(); } }Pawl-0.3.4/src/functions.php000066400000000000000000000016671341711404000157360ustar00rootroot00000000000000 */ function connect($url, array $subProtocols = [], $headers = [], LoopInterface $loop = null) { $loop = $loop ?: ReactFactory::create(); $connector = new Connector($loop); $connection = $connector($url, $subProtocols, $headers); $runHasBeenCalled = false; $loop->addTimer(Timer::MIN_INTERVAL, function () use (&$runHasBeenCalled) { $runHasBeenCalled = true; }); register_shutdown_function(function() use ($loop, &$runHasBeenCalled) { if (!$runHasBeenCalled) { $loop->run(); } }); return $connection; } Pawl-0.3.4/src/functions_include.php000066400000000000000000000001421341711404000174240ustar00rootroot00000000000000then(function(WebSocket $conn) { $futureNum = new Deferred; $conn->on('message', function($msg) use ($futureNum) { $futureNum->resolve($msg); }); return $futureNum->promise(); }, function($e) { echo "Could not connect to test server: {$e->getMessage()}\n"; })->then(function($numOfCases) use ($connector, $loop) { echo "Running {$numOfCases} test cases\n\n"; $allCases = new Deferred; $i = 0; $runNextCase = function() use (&$runNextCase, &$i, $numOfCases, $allCases, $connector, $loop) { $i++; if ($i > (int)$numOfCases->getPayload()) { $allCases->resolve(); return; } echo "."; $connector("/runCase?case={$i}&agent=" . AGENT)->then(function(WebSocket $conn) use ($runNextCase) { $conn->on('message', function($msg, $conn) { $conn->send($msg); }); $conn->on('close', $runNextCase); }); }; $runNextCase(); return $allCases->promise(); })->then(function() use ($connector, $loop) { $connector('/updateReports?agent=' . AGENT)->then(function(WebSocket $conn) use ($loop) { echo "\nDone!\n"; $conn->on('close', [$loop, 'stop']); }); }); $loop->run(); Pawl-0.3.4/tests/bootstrap.php000066400000000000000000000000641341711404000163040ustar00rootroot00000000000000getMock('React\Socket\ConnectorInterface'); $connector->expects($this->once()) ->method('connect') ->with($this->callback(function ($uri) use ($expectedConnectorUri) { return $uri === $expectedConnectorUri; })) // reject the promise so that we don't have to mock a connection here ->willReturn(new RejectedPromise(new Exception(''))); $pawlConnector = new Connector($loop, $connector); $pawlConnector($uri); } public function testConnectorRejectsWhenUnderlyingSocketConnectorRejects() { $exception = new RuntimeException('Connection failed'); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); $connector->expects($this->once())->method('connect')->willReturn(\React\Promise\reject($exception)); $pawlConnector = new Connector($loop, $connector); $promise = $pawlConnector('ws://localhost'); $actual = null; $promise->then(null, function ($reason) use (&$actual) { $actual = $reason; }); $this->assertSame($exception, $actual); } public function testCancelConnectorShouldCancelUnderlyingSocketConnectorWhenSocketConnectionIsPending() { $promise = new Promise(function () { }, function () use (&$cancelled) { ++$cancelled; }); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); $connector->expects($this->once())->method('connect')->willReturn($promise); $pawlConnector = new Connector($loop, $connector); $promise = $pawlConnector('ws://localhost'); $this->assertNull($cancelled); $promise->cancel(); $this->assertEquals(1, $cancelled); $message = null; $promise->then(null, function ($reason) use (&$message) { $message = $reason->getMessage(); }); $this->assertEquals('Connection to ws://localhost cancelled during handshake', $message); } public function testCancelConnectorShouldCloseUnderlyingSocketConnectionWhenHandshakeIsPending() { $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $connection->expects($this->once())->method('close'); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); $connector->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); $pawlConnector = new Connector($loop, $connector); $promise = $pawlConnector('ws://localhost'); $promise->cancel(); $message = null; $promise->then(null, function ($reason) use (&$message) { $message = $reason->getMessage(); }); $this->assertEquals('Connection to ws://localhost cancelled during handshake', $message); } } Pawl-0.3.4/tests/unit/RequestUriTest.php000066400000000000000000000023631341711404000202220ustar00rootroot00000000000000getMethod($methodName); $method->setAccessible(true); return $method; } function uriDataProvider() { return [ ['ws://127.0.0.1/bla', 'http://127.0.0.1/bla'], ['wss://127.0.0.1/bla', 'https://127.0.0.1/bla'], ['ws://127.0.0.1:1234/bla', 'http://127.0.0.1:1234/bla'], ['wss://127.0.0.1:4321/bla', 'https://127.0.0.1:4321/bla'] ]; } /** * @dataProvider uriDataProvider */ function testGeneratedRequestUri($uri, $expectedRequestUri) { $loop = Factory::create(); $connector = new Connector($loop); $generateRequest = self::getPrivateClassMethod('\Ratchet\Client\Connector', 'generateRequest'); $request = $generateRequest->invokeArgs($connector, [$uri, [], []]); $this->assertEquals((string)$request->getUri(), $expectedRequestUri); } }