aboutsummaryrefslogtreecommitdiffstats
path: root/Source/Client.php
diff options
context:
space:
mode:
Diffstat (limited to 'Source/Client.php')
-rw-r--r--Source/Client.php341
1 files changed, 341 insertions, 0 deletions
diff --git a/Source/Client.php b/Source/Client.php
new file mode 100644
index 0000000..4a8969b
--- /dev/null
+++ b/Source/Client.php
@@ -0,0 +1,341 @@
+<?php
+
+/**
+ * Hoa
+ *
+ *
+ * @license
+ *
+ * New BSD License
+ *
+ * Copyright © 2007-2017, Hoa community. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * Neither the name of the Hoa nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+namespace Hoa\Websocket;
+
+use Hoa\Consistency;
+use Hoa\Event;
+use Hoa\Http;
+use Hoa\Socket as HoaSocket;
+
+/**
+ * Class \Hoa\Websocket\Client.
+ *
+ * A cross-protocol Websocket client.
+ *
+ * @copyright Copyright © 2007-2017 Hoa community
+ * @license New BSD License
+ */
+class Client extends Connection
+{
+ /**
+ * Endpoint.
+ *
+ * @var string
+ */
+ protected $_endPoint = null;
+
+ /**
+ * Host name.
+ *
+ * @var string
+ */
+ protected $_host = null;
+
+ /**
+ * Response (mainly parser).
+ *
+ * @var \Hoa\Http\Response
+ */
+ protected $_response = null;
+
+
+
+ /**
+ * Create a Websocket client.
+ *
+ * @param \Hoa\Socket\Client $client Client.
+ * @param string $endPoint End-point.
+ * @param \Hoa\Http\Response $request Response parser.
+ * @throws \Hoa\Socket\Exception
+ */
+ public function __construct(
+ HoaSocket\Client $client,
+ $endPoint = null,
+ Http\Response $response = null
+ ) {
+ parent::__construct($client);
+
+ if (null === $endPoint) {
+ $endPoint = '/';
+ $socket = $client->getSocket();
+
+ if ($socket instanceof Socket) {
+ $endPoint = $socket->getEndPoint();
+ }
+ }
+
+ $this->setEndPoint($endPoint);
+
+ if (null === $response) {
+ $response = new Http\Response(false);
+ }
+
+ $this->setResponse($response);
+
+ return;
+ }
+
+ /**
+ * Connect, i.e. open the connection and do handshake.
+ *
+ * @return void
+ */
+ public function connect()
+ {
+ return $this->doHandshake();
+ }
+
+ /**
+ * Override the parent run() method to open the connection.
+ *
+ * @return void
+ */
+ public function run()
+ {
+ $this->connect();
+
+ return parent::run();
+ }
+
+ /**
+ * Receive data. Fire listeners.
+ *
+ * @return void
+ */
+ public function receive()
+ {
+ $connection = $this->getConnection();
+ $node = $connection->getCurrentNode();
+
+ do {
+ $this->_run($node);
+ } while (
+ false === $connection->isDisconnected() &&
+ true !== $node->isMessageComplete()
+ );
+ }
+
+ /**
+ * Try the handshake by trying different protocol implementation.
+ *
+ * @return void
+ * @throws \Hoa\Websocket\Exception
+ * @throws \Hoa\Websocket\Exception\BadProtocol
+ */
+ protected function doHandshake()
+ {
+ $connection = $this->getConnection();
+ $connection->connect();
+
+ if (true === $connection->getSocket()->isSecured()) {
+ $connection->enableEncryption(true, $connection::ENCRYPTION_TLS);
+ }
+
+ $connection->setStreamBlocking(true);
+
+ $key = $this->getNewChallenge();
+ $expected = base64_encode(sha1($key . Protocol\Rfc6455::GUID, true));
+
+ if (null === $host = $this->getHost()) {
+ throw new Exception(
+ 'Host name is null. Please, use the %s::setHost() method.',
+ 0,
+ __CLASS__
+ );
+ }
+
+ $connection->writeAll(
+ $request =
+ 'GET ' . $this->getEndPoint() . ' HTTP/1.1' . CRLF .
+ 'Host: ' . $host . CRLF .
+ 'User-Agent: Hoa' . CRLF .
+ 'Upgrade: WebSocket' . CRLF .
+ 'Connection: Upgrade' . CRLF .
+ 'Pragma: no-cache' . CRLF .
+ 'Cache-Control: no-cache' . CRLF .
+ 'Sec-WebSocket-Key: ' . $key . CRLF .
+ 'Sec-WebSocket-Version: 13' . CRLF . CRLF
+ );
+
+ $buffer = $connection->read(2048);
+ $response = $this->getResponse();
+ $response->parse($buffer);
+
+ if ($response::STATUS_SWITCHING_PROTOCOLS !== $response['status'] ||
+ 'websocket' !== strtolower($response['upgrade']) ||
+ 'upgrade' !== strtolower($response['connection']) ||
+ $expected !== $response['sec-websocket-accept']) {
+ throw new Exception\BadProtocol(
+ 'Handshake has failed, the server did not return a valid ' .
+ 'response.' . "\n\n" .
+ 'Client:' . "\n" . ' %s' . "\n" .
+ 'Server:' . "\n" . ' %s',
+ 0,
+ [
+ str_replace("\n", "\n" . ' ', $request),
+ str_replace("\n", "\n" . ' ', $buffer)
+ ]
+ );
+ }
+
+ $currentNode = $connection->getCurrentNode();
+ $currentNode->setHandshake(SUCCEED);
+ $currentNode->setProtocolImplementation(new Protocol\Rfc6455($connection));
+
+ $this->getListener()->fire(
+ 'open',
+ new Event\Bucket()
+ );
+
+ return;
+ }
+
+ /**
+ * Generate a challenge.
+ *
+ * @return string
+ */
+ public function getNewChallenge()
+ {
+ static $_tail = ['A', 'Q', 'g', 'w'];
+
+ return
+ substr(base64_encode(Consistency::uuid()), 0, 21) .
+ $_tail[mt_rand(0, 3)] . '==';
+ }
+
+ /**
+ * Close a specific node/connection.
+ *
+ * @param int $code Code (please, see
+ * self::CLOSE_* constants).
+ * @param string $reason Reason.
+ * @return void
+ */
+ public function close($code = self::CLOSE_NORMAL, $reason = null)
+ {
+ $connection = $this->getConnection();
+ $protocol = $connection->getCurrentNode()->getProtocolImplementation();
+
+ if (null !== $protocol) {
+ $protocol->close($code, $reason, true);
+ }
+
+ $connection->mute();
+ $connection->setStreamTimeout(0, 2 * 15000); // 2 * MLS (on FreeBSD)
+ $connection->read(1);
+
+ return $connection->disconnect();
+ }
+
+ /**
+ * Set end-point.
+ *
+ * @param string $endPoint End-point.
+ * @return string
+ */
+ protected function setEndPoint($endPoint)
+ {
+ $old = $this->_endPoint;
+ $this->_endPoint = $endPoint;
+
+ return $old;
+ }
+
+ /**
+ * Get end-point.
+ *
+ * @return string
+ */
+ public function getEndPoint()
+ {
+ return $this->_endPoint;
+ }
+
+ /**
+ * Set response (mainly parser).
+ *
+ * @param \Hoa\Http\Response $response Response.
+ * @return \Hoa\Http\Response
+ */
+ public function setResponse(Http\Response $response)
+ {
+ $old = $this->_response;
+ $this->_response = $response;
+
+ return $old;
+ }
+
+ /**
+ * Get response.
+ *
+ * @return \Hoa\Http\Response
+ */
+ public function getResponse()
+ {
+ return $this->_response;
+ }
+
+ /**
+ * Set host.
+ *
+ * @param string $host Host.
+ * @return string
+ */
+ public function setHost($host)
+ {
+ $old = $this->_host;
+ $this->_host = $host;
+
+ return $old;
+ }
+
+ /**
+ * Get host.
+ *
+ * @return string
+ */
+ public function getHost()
+ {
+ return
+ null !== $this->_host
+ ? $this->_host
+ : (isset($_SERVER['HTTP_HOST'])
+ ? $_SERVER['HTTP_HOST']
+ : null);
+ }
+}