summaryrefslogtreecommitdiffstats
path: root/vendor/textalk/websocket/lib
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/textalk/websocket/lib')
-rw-r--r--vendor/textalk/websocket/lib/BadOpcodeException.php7
-rw-r--r--vendor/textalk/websocket/lib/BadUriException.php7
-rw-r--r--vendor/textalk/websocket/lib/Base.php486
-rw-r--r--vendor/textalk/websocket/lib/Client.php216
-rw-r--r--vendor/textalk/websocket/lib/ConnectionException.php26
-rw-r--r--vendor/textalk/websocket/lib/Exception.php7
-rw-r--r--vendor/textalk/websocket/lib/Message/Binary.php8
-rw-r--r--vendor/textalk/websocket/lib/Message/Close.php8
-rw-r--r--vendor/textalk/websocket/lib/Message/Factory.php25
-rw-r--r--vendor/textalk/websocket/lib/Message/Message.php53
-rw-r--r--vendor/textalk/websocket/lib/Message/Ping.php8
-rw-r--r--vendor/textalk/websocket/lib/Message/Pong.php8
-rw-r--r--vendor/textalk/websocket/lib/Message/Text.php8
-rw-r--r--vendor/textalk/websocket/lib/Server.php176
-rw-r--r--vendor/textalk/websocket/lib/TimeoutException.php7
15 files changed, 1050 insertions, 0 deletions
diff --git a/vendor/textalk/websocket/lib/BadOpcodeException.php b/vendor/textalk/websocket/lib/BadOpcodeException.php
new file mode 100644
index 0000000..a518715
--- /dev/null
+++ b/vendor/textalk/websocket/lib/BadOpcodeException.php
@@ -0,0 +1,7 @@
+<?php
+
+namespace WebSocket;
+
+class BadOpcodeException extends Exception
+{
+}
diff --git a/vendor/textalk/websocket/lib/BadUriException.php b/vendor/textalk/websocket/lib/BadUriException.php
new file mode 100644
index 0000000..39e975d
--- /dev/null
+++ b/vendor/textalk/websocket/lib/BadUriException.php
@@ -0,0 +1,7 @@
+<?php
+
+namespace WebSocket;
+
+class BadUriException extends Exception
+{
+}
diff --git a/vendor/textalk/websocket/lib/Base.php b/vendor/textalk/websocket/lib/Base.php
new file mode 100644
index 0000000..f460289
--- /dev/null
+++ b/vendor/textalk/websocket/lib/Base.php
@@ -0,0 +1,486 @@
+<?php
+
+/**
+ * Copyright (C) 2014-2020 Textalk/Abicart and contributors.
+ *
+ * This file is part of Websocket PHP and is free software under the ISC License.
+ * License text: https://raw.githubusercontent.com/Textalk/websocket-php/master/COPYING
+ */
+
+namespace WebSocket;
+
+use Psr\Log\{LoggerAwareInterface, LoggerInterface, NullLogger};
+use WebSocket\Message\Factory;
+
+class Base implements LoggerAwareInterface
+{
+ protected $socket;
+ protected $options = [];
+ protected $is_closing = false;
+ protected $last_opcode = null;
+ protected $close_status = null;
+ protected $logger;
+ private $read_buffer;
+
+ protected static $opcodes = [
+ 'continuation' => 0,
+ 'text' => 1,
+ 'binary' => 2,
+ 'close' => 8,
+ 'ping' => 9,
+ 'pong' => 10,
+ ];
+
+ public function getLastOpcode(): ?string
+ {
+ return $this->last_opcode;
+ }
+
+ public function getCloseStatus(): ?int
+ {
+ return $this->close_status;
+ }
+
+ public function isConnected(): bool
+ {
+ return $this->socket &&
+ (get_resource_type($this->socket) == 'stream' ||
+ get_resource_type($this->socket) == 'persistent stream');
+ }
+
+ public function setTimeout(int $timeout): void
+ {
+ $this->options['timeout'] = $timeout;
+
+ if ($this->isConnected()) {
+ stream_set_timeout($this->socket, $timeout);
+ }
+ }
+
+ public function setFragmentSize(int $fragment_size): self
+ {
+ $this->options['fragment_size'] = $fragment_size;
+ return $this;
+ }
+
+ public function getFragmentSize(): int
+ {
+ return $this->options['fragment_size'];
+ }
+
+ public function setLogger(LoggerInterface $logger = null): void
+ {
+ $this->logger = $logger ?: new NullLogger();
+ }
+
+ public function send(string $payload, string $opcode = 'text', bool $masked = true): void
+ {
+ if (!$this->isConnected()) {
+ $this->connect();
+ }
+
+ if (!in_array($opcode, array_keys(self::$opcodes))) {
+ $warning = "Bad opcode '{$opcode}'. Try 'text' or 'binary'.";
+ $this->logger->warning($warning);
+ throw new BadOpcodeException($warning);
+ }
+
+ $payload_chunks = str_split($payload, $this->options['fragment_size']);
+ $frame_opcode = $opcode;
+
+ for ($index = 0; $index < count($payload_chunks); ++$index) {
+ $chunk = $payload_chunks[$index];
+ $final = $index == count($payload_chunks) - 1;
+
+ $this->sendFragment($final, $chunk, $frame_opcode, $masked);
+
+ // all fragments after the first will be marked a continuation
+ $frame_opcode = 'continuation';
+ }
+
+ $this->logger->info("Sent '{$opcode}' message", [
+ 'opcode' => $opcode,
+ 'content-length' => strlen($payload),
+ 'frames' => count($payload_chunks),
+ ]);
+ }
+
+ /**
+ * Convenience method to send text message
+ * @param string $payload Content as string
+ */
+ public function text(string $payload): void
+ {
+ $this->send($payload);
+ }
+
+ /**
+ * Convenience method to send binary message
+ * @param string $payload Content as binary string
+ */
+ public function binary(string $payload): void
+ {
+ $this->send($payload, 'binary');
+ }
+
+ /**
+ * Convenience method to send ping
+ * @param string $payload Optional text as string
+ */
+ public function ping(string $payload = ''): void
+ {
+ $this->send($payload, 'ping');
+ }
+
+ /**
+ * Convenience method to send unsolicited pong
+ * @param string $payload Optional text as string
+ */
+ public function pong(string $payload = ''): void
+ {
+ $this->send($payload, 'pong');
+ }
+
+ /**
+ * Get name of local socket, or null if not connected
+ * @return string|null
+ */
+ public function getName(): ?string
+ {
+ return $this->isConnected() ? stream_socket_get_name($this->socket, false) : null;
+ }
+
+ /**
+ * Get name of remote socket, or null if not connected
+ * @return string|null
+ */
+ public function getPier(): ?string
+ {
+ return $this->isConnected() ? stream_socket_get_name($this->socket, true) : null;
+ }
+
+ /**
+ * Get string representation of instance
+ * @return string String representation
+ */
+ public function __toString(): string
+ {
+ return sprintf(
+ "%s(%s)",
+ get_class($this),
+ $this->getName() ?: 'closed'
+ );
+ }
+
+ /**
+ * Receive one message.
+ * Will continue reading until read message match filter settings.
+ * Return Message instance or string according to settings.
+ */
+ protected function sendFragment(bool $final, string $payload, string $opcode, bool $masked): void
+ {
+ $data = '';
+
+ $byte_1 = $final ? 0b10000000 : 0b00000000; // Final fragment marker.
+ $byte_1 |= self::$opcodes[$opcode]; // Set opcode.
+ $data .= pack('C', $byte_1);
+
+ $byte_2 = $masked ? 0b10000000 : 0b00000000; // Masking bit marker.
+
+ // 7 bits of payload length...
+ $payload_length = strlen($payload);
+ if ($payload_length > 65535) {
+ $data .= pack('C', $byte_2 | 0b01111111);
+ $data .= pack('J', $payload_length);
+ } elseif ($payload_length > 125) {
+ $data .= pack('C', $byte_2 | 0b01111110);
+ $data .= pack('n', $payload_length);
+ } else {
+ $data .= pack('C', $byte_2 | $payload_length);
+ }
+
+ // Handle masking
+ if ($masked) {
+ // generate a random mask:
+ $mask = '';
+ for ($i = 0; $i < 4; $i++) {
+ $mask .= chr(rand(0, 255));
+ }
+ $data .= $mask;
+
+ // Append payload to frame:
+ for ($i = 0; $i < $payload_length; $i++) {
+ $data .= $payload[$i] ^ $mask[$i % 4];
+ }
+ } else {
+ $data .= $payload;
+ }
+
+ $this->write($data);
+ $this->logger->debug("Sent '{$opcode}' frame", [
+ 'opcode' => $opcode,
+ 'final' => $final,
+ 'content-length' => strlen($payload),
+ ]);
+ }
+
+ public function receive()
+ {
+ $filter = $this->options['filter'];
+ if (!$this->isConnected()) {
+ $this->connect();
+ }
+
+ do {
+ $response = $this->receiveFragment();
+ list ($payload, $final, $opcode) = $response;
+
+ // Continuation and factual opcode
+ $continuation = ($opcode == 'continuation');
+ $payload_opcode = $continuation ? $this->read_buffer['opcode'] : $opcode;
+
+ // Filter frames
+ if (!in_array($payload_opcode, $filter)) {
+ if ($payload_opcode == 'close') {
+ return null; // Always abort receive on close
+ }
+ $final = false;
+ continue; // Continue reading
+ }
+
+ // First continuation frame, create buffer
+ if (!$final && !$continuation) {
+ $this->read_buffer = ['opcode' => $opcode, 'payload' => $payload, 'frames' => 1];
+ continue; // Continue reading
+ }
+
+ // Subsequent continuation frames, add to buffer
+ if ($continuation) {
+ $this->read_buffer['payload'] .= $payload;
+ $this->read_buffer['frames']++;
+ }
+ } while (!$final);
+
+ // Final, return payload
+ $frames = 1;
+ if ($continuation) {
+ $payload = $this->read_buffer['payload'];
+ $frames = $this->read_buffer['frames'];
+ $this->read_buffer = null;
+ }
+ $this->logger->info("Received '{opcode}' message", [
+ 'opcode' => $payload_opcode,
+ 'content-length' => strlen($payload),
+ 'frames' => $frames,
+ ]);
+
+ $this->last_opcode = $payload_opcode;
+ $factory = new Factory();
+ return $this->options['return_obj']
+ ? $factory->create($payload_opcode, $payload)
+ : $payload;
+ }
+
+ protected function receiveFragment(): array
+ {
+ // Read the fragment "header" first, two bytes.
+ $data = $this->read(2);
+ list ($byte_1, $byte_2) = array_values(unpack('C*', $data));
+
+ $final = (bool)($byte_1 & 0b10000000); // Final fragment marker.
+ $rsv = $byte_1 & 0b01110000; // Unused bits, ignore
+
+ // Parse opcode
+ $opcode_int = $byte_1 & 0b00001111;
+ $opcode_ints = array_flip(self::$opcodes);
+ if (!array_key_exists($opcode_int, $opcode_ints)) {
+ $warning = "Bad opcode in websocket frame: {$opcode_int}";
+ $this->logger->warning($warning);
+ throw new ConnectionException($warning, ConnectionException::BAD_OPCODE);
+ }
+ $opcode = $opcode_ints[$opcode_int];
+
+ // Masking bit
+ $mask = (bool)($byte_2 & 0b10000000);
+
+ $payload = '';
+
+ // Payload length
+ $payload_length = $byte_2 & 0b01111111;
+
+ if ($payload_length > 125) {
+ if ($payload_length === 126) {
+ $data = $this->read(2); // 126: Payload is a 16-bit unsigned int
+ $payload_length = current(unpack('n', $data));
+ } else {
+ $data = $this->read(8); // 127: Payload is a 64-bit unsigned int
+ $payload_length = current(unpack('J', $data));
+ }
+ }
+
+ // Get masking key.
+ if ($mask) {
+ $masking_key = $this->read(4);
+ }
+
+ // Get the actual payload, if any (might not be for e.g. close frames.
+ if ($payload_length > 0) {
+ $data = $this->read($payload_length);
+
+ if ($mask) {
+ // Unmask payload.
+ for ($i = 0; $i < $payload_length; $i++) {
+ $payload .= ($data[$i] ^ $masking_key[$i % 4]);
+ }
+ } else {
+ $payload = $data;
+ }
+ }
+
+ $this->logger->debug("Read '{opcode}' frame", [
+ 'opcode' => $opcode,
+ 'final' => $final,
+ 'content-length' => strlen($payload),
+ ]);
+
+ // if we received a ping, send a pong and wait for the next message
+ if ($opcode === 'ping') {
+ $this->logger->debug("Received 'ping', sending 'pong'.");
+ $this->send($payload, 'pong', true);
+ return [$payload, true, $opcode];
+ }
+
+ // if we received a pong, wait for the next message
+ if ($opcode === 'pong') {
+ $this->logger->debug("Received 'pong'.");
+ return [$payload, true, $opcode];
+ }
+
+ if ($opcode === 'close') {
+ $status_bin = '';
+ $status = '';
+ // Get the close status.
+ $status_bin = '';
+ $status = '';
+ if ($payload_length > 0) {
+ $status_bin = $payload[0] . $payload[1];
+ $status = current(unpack('n', $payload));
+ $this->close_status = $status;
+ }
+ // Get additional close message
+ if ($payload_length >= 2) {
+ $payload = substr($payload, 2);
+ }
+
+ $this->logger->debug("Received 'close', status: {$this->close_status}.");
+
+ if ($this->is_closing) {
+ $this->is_closing = false; // A close response, all done.
+ } else {
+ $this->send($status_bin . 'Close acknowledged: ' . $status, 'close', true); // Respond.
+ }
+
+ // Close the socket.
+ fclose($this->socket);
+
+ // Closing should not return message.
+ return [$payload, true, $opcode];
+ }
+
+ return [$payload, $final, $opcode];
+ }
+
+ /**
+ * Tell the socket to close.
+ *
+ * @param integer $status http://tools.ietf.org/html/rfc6455#section-7.4
+ * @param string $message A closing message, max 125 bytes.
+ */
+ public function close(int $status = 1000, string $message = 'ttfn'): void
+ {
+ if (!$this->isConnected()) {
+ return;
+ }
+ $status_binstr = sprintf('%016b', $status);
+ $status_str = '';
+ foreach (str_split($status_binstr, 8) as $binstr) {
+ $status_str .= chr(bindec($binstr));
+ }
+ $this->send($status_str . $message, 'close', true);
+ $this->logger->debug("Closing with status: {$status_str}.");
+
+ $this->is_closing = true;
+ $this->receive(); // Receiving a close frame will close the socket now.
+ }
+
+ /**
+ * Disconnect from client/server.
+ */
+ public function disconnect(): void
+ {
+ if ($this->isConnected()) {
+ fclose($this->socket);
+ $this->socket = null;
+ }
+ }
+
+ protected function write(string $data): void
+ {
+ $length = strlen($data);
+ $written = @fwrite($this->socket, $data);
+ if ($written === false) {
+ $this->throwException("Failed to write {$length} bytes.");
+ }
+ if ($written < strlen($data)) {
+ $this->throwException("Could only write {$written} out of {$length} bytes.");
+ }
+ $this->logger->debug("Wrote {$written} of {$length} bytes.");
+ }
+
+ protected function read(string $length): string
+ {
+ $data = '';
+ while (strlen($data) < $length) {
+ $buffer = @fread($this->socket, $length - strlen($data));
+
+ if (!$buffer) {
+ $meta = stream_get_meta_data($this->socket);
+ if (!empty($meta['timed_out'])) {
+ $message = 'Client read timeout';
+ $this->logger->error($message, $meta);
+ throw new TimeoutException($message, ConnectionException::TIMED_OUT, $meta);
+ }
+ }
+ if ($buffer === false) {
+ $read = strlen($data);
+ $this->throwException("Broken frame, read {$read} of stated {$length} bytes.");
+ }
+ if ($buffer === '') {
+ $this->throwException("Empty read; connection dead?");
+ }
+ $data .= $buffer;
+ $read = strlen($data);
+ $this->logger->debug("Read {$read} of {$length} bytes.");
+ }
+ return $data;
+ }
+
+ protected function throwException(string $message, int $code = 0): void
+ {
+ $meta = ['closed' => true];
+ if ($this->isConnected()) {
+ $meta = stream_get_meta_data($this->socket);
+ fclose($this->socket);
+ $this->socket = null;
+ }
+ if (!empty($meta['timed_out'])) {
+ $this->logger->error($message, $meta);
+ throw new TimeoutException($message, ConnectionException::TIMED_OUT, $meta);
+ }
+ if (!empty($meta['eof'])) {
+ $code = ConnectionException::EOF;
+ }
+ $this->logger->error($message, $meta);
+ throw new ConnectionException($message, $code, $meta);
+ }
+}
diff --git a/vendor/textalk/websocket/lib/Client.php b/vendor/textalk/websocket/lib/Client.php
new file mode 100644
index 0000000..8cefaaa
--- /dev/null
+++ b/vendor/textalk/websocket/lib/Client.php
@@ -0,0 +1,216 @@
+<?php
+
+/**
+ * Copyright (C) 2014-2020 Textalk/Abicart and contributors.
+ *
+ * This file is part of Websocket PHP and is free software under the ISC License.
+ * License text: https://raw.githubusercontent.com/Textalk/websocket-php/master/COPYING
+ */
+
+namespace WebSocket;
+
+class Client extends Base
+{
+ // Default options
+ protected static $default_options = [
+ 'context' => null,
+ 'filter' => ['text', 'binary'],
+ 'fragment_size' => 4096,
+ 'headers' => null,
+ 'logger' => null,
+ 'origin' => null, // @deprecated
+ 'persistent' => false,
+ 'return_obj' => false,
+ 'timeout' => 5,
+ ];
+
+ protected $socket_uri;
+
+ /**
+ * @param string $uri A ws/wss-URI
+ * @param array $options
+ * Associative array containing:
+ * - context: Set the stream context. Default: empty context
+ * - timeout: Set the socket timeout in seconds. Default: 5
+ * - fragment_size: Set framgemnt size. Default: 4096
+ * - headers: Associative array of headers to set/override.
+ */
+ public function __construct(string $uri, array $options = [])
+ {
+ $this->options = array_merge(self::$default_options, $options);
+ $this->socket_uri = $uri;
+ $this->setLogger($this->options['logger']);
+ }
+
+ public function __destruct()
+ {
+ if ($this->isConnected() && get_resource_type($this->socket) !== 'persistent stream') {
+ fclose($this->socket);
+ }
+ $this->socket = null;
+ }
+
+ /**
+ * Perform WebSocket handshake
+ */
+ protected function connect(): void
+ {
+ $url_parts = parse_url($this->socket_uri);
+ if (empty($url_parts) || empty($url_parts['scheme']) || empty($url_parts['host'])) {
+ $error = "Invalid url '{$this->socket_uri}' provided.";
+ $this->logger->error($error);
+ throw new BadUriException($error);
+ }
+ $scheme = $url_parts['scheme'];
+ $host = $url_parts['host'];
+ $user = isset($url_parts['user']) ? $url_parts['user'] : '';
+ $pass = isset($url_parts['pass']) ? $url_parts['pass'] : '';
+ $port = isset($url_parts['port']) ? $url_parts['port'] : ($scheme === 'wss' ? 443 : 80);
+ $path = isset($url_parts['path']) ? $url_parts['path'] : '/';
+ $query = isset($url_parts['query']) ? $url_parts['query'] : '';
+ $fragment = isset($url_parts['fragment']) ? $url_parts['fragment'] : '';
+
+ $path_with_query = $path;
+ if (!empty($query)) {
+ $path_with_query .= '?' . $query;
+ }
+ if (!empty($fragment)) {
+ $path_with_query .= '#' . $fragment;
+ }
+
+ if (!in_array($scheme, ['ws', 'wss'])) {
+ $error = "Url should have scheme ws or wss, not '{$scheme}' from URI '{$this->socket_uri}'.";
+ $this->logger->error($error);
+ throw new BadUriException($error);
+ }
+
+ $host_uri = ($scheme === 'wss' ? 'ssl' : 'tcp') . '://' . $host;
+
+ // Set the stream context options if they're already set in the config
+ if (isset($this->options['context'])) {
+ // Suppress the error since we'll catch it below
+ if (@get_resource_type($this->options['context']) === 'stream-context') {
+ $context = $this->options['context'];
+ } else {
+ $error = "Stream context in \$options['context'] isn't a valid context.";
+ $this->logger->error($error);
+ throw new \InvalidArgumentException($error);
+ }
+ } else {
+ $context = stream_context_create();
+ }
+
+ $persistent = $this->options['persistent'] === true;
+ $flags = STREAM_CLIENT_CONNECT;
+ $flags = $persistent ? $flags | STREAM_CLIENT_PERSISTENT : $flags;
+
+ $error = $errno = $errstr = null;
+ set_error_handler(function (int $severity, string $message, string $file, int $line) use (&$error) {
+ $this->logger->warning($message, ['severity' => $severity]);
+ $error = $message;
+ }, E_ALL);
+
+ // Open the socket.
+ $this->socket = stream_socket_client(
+ "{$host_uri}:{$port}",
+ $errno,
+ $errstr,
+ $this->options['timeout'],
+ $flags,
+ $context
+ );
+
+ restore_error_handler();
+
+ if (!$this->isConnected()) {
+ $error = "Could not open socket to \"{$host}:{$port}\": {$errstr} ({$errno}) {$error}.";
+ $this->logger->error($error);
+ throw new ConnectionException($error);
+ }
+
+ $address = "{$scheme}://{$host}{$path_with_query}";
+
+ if (!$persistent || ftell($this->socket) == 0) {
+ // Set timeout on the stream as well.
+ stream_set_timeout($this->socket, $this->options['timeout']);
+
+ // Generate the WebSocket key.
+ $key = self::generateKey();
+
+ // Default headers
+ $headers = [
+ 'Host' => $host . ":" . $port,
+ 'User-Agent' => 'websocket-client-php',
+ 'Connection' => 'Upgrade',
+ 'Upgrade' => 'websocket',
+ 'Sec-WebSocket-Key' => $key,
+ 'Sec-WebSocket-Version' => '13',
+ ];
+
+ // Handle basic authentication.
+ if ($user || $pass) {
+ $headers['authorization'] = 'Basic ' . base64_encode($user . ':' . $pass);
+ }
+
+ // Deprecated way of adding origin (use headers instead).
+ if (isset($this->options['origin'])) {
+ $headers['origin'] = $this->options['origin'];
+ }
+
+ // Add and override with headers from options.
+ if (isset($this->options['headers'])) {
+ $headers = array_merge($headers, $this->options['headers']);
+ }
+
+ $header = "GET " . $path_with_query . " HTTP/1.1\r\n" . implode(
+ "\r\n",
+ array_map(
+ function ($key, $value) {
+ return "$key: $value";
+ },
+ array_keys($headers),
+ $headers
+ )
+ ) . "\r\n\r\n";
+
+ // Send headers.
+ $this->write($header);
+
+ // Get server response header (terminated with double CR+LF).
+ $response = stream_get_line($this->socket, 1024, "\r\n\r\n");
+
+ // Validate response.
+ if (!preg_match('#Sec-WebSocket-Accept:\s(.*)$#mUi', $response, $matches)) {
+ $error = "Connection to '{$address}' failed: Server sent invalid upgrade response: {$response}";
+ $this->logger->error($error);
+ throw new ConnectionException($error);
+ }
+
+ $keyAccept = trim($matches[1]);
+ $expectedResonse
+ = base64_encode(pack('H*', sha1($key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')));
+
+ if ($keyAccept !== $expectedResonse) {
+ $error = 'Server sent bad upgrade response.';
+ $this->logger->error($error);
+ throw new ConnectionException($error);
+ }
+ }
+
+ $this->logger->info("Client connected to {$address}");
+ }
+
+ /**
+ * Generate a random string for WebSocket key.
+ *
+ * @return string Random string
+ */
+ protected static function generateKey(): string
+ {
+ $key = '';
+ for ($i = 0; $i < 16; $i++) {
+ $key .= chr(rand(33, 126));
+ }
+ return base64_encode($key);
+ }
+}
diff --git a/vendor/textalk/websocket/lib/ConnectionException.php b/vendor/textalk/websocket/lib/ConnectionException.php
new file mode 100644
index 0000000..7e1ecbf
--- /dev/null
+++ b/vendor/textalk/websocket/lib/ConnectionException.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace WebSocket;
+
+use Throwable;
+
+class ConnectionException extends Exception
+{
+ // Native codes in interval 0-106
+ public const TIMED_OUT = 1024;
+ public const EOF = 1025;
+ public const BAD_OPCODE = 1026;
+
+ private $data;
+
+ public function __construct(string $message, int $code = 0, array $data = [], Throwable $prev = null)
+ {
+ parent::__construct($message, $code, $prev);
+ $this->data = $data;
+ }
+
+ public function getData(): array
+ {
+ return $this->data;
+ }
+}
diff --git a/vendor/textalk/websocket/lib/Exception.php b/vendor/textalk/websocket/lib/Exception.php
new file mode 100644
index 0000000..6482b7e
--- /dev/null
+++ b/vendor/textalk/websocket/lib/Exception.php
@@ -0,0 +1,7 @@
+<?php
+
+namespace WebSocket;
+
+class Exception extends \Exception
+{
+}
diff --git a/vendor/textalk/websocket/lib/Message/Binary.php b/vendor/textalk/websocket/lib/Message/Binary.php
new file mode 100644
index 0000000..84d27f5
--- /dev/null
+++ b/vendor/textalk/websocket/lib/Message/Binary.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace WebSocket\Message;
+
+class Binary extends Message
+{
+ protected $opcode = 'binary';
+}
diff --git a/vendor/textalk/websocket/lib/Message/Close.php b/vendor/textalk/websocket/lib/Message/Close.php
new file mode 100644
index 0000000..b759fe9
--- /dev/null
+++ b/vendor/textalk/websocket/lib/Message/Close.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace WebSocket\Message;
+
+class Close extends Message
+{
+ protected $opcode = 'close';
+}
diff --git a/vendor/textalk/websocket/lib/Message/Factory.php b/vendor/textalk/websocket/lib/Message/Factory.php
new file mode 100644
index 0000000..31df89c
--- /dev/null
+++ b/vendor/textalk/websocket/lib/Message/Factory.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace WebSocket\Message;
+
+use WebSocket\BadOpcodeException;
+
+class Factory
+{
+ public function create(string $opcode, string $payload = ''): Message
+ {
+ switch ($opcode) {
+ case 'text':
+ return new Text($payload);
+ case 'binary':
+ return new Binary($payload);
+ case 'ping':
+ return new Ping($payload);
+ case 'pong':
+ return new Pong($payload);
+ case 'close':
+ return new Close($payload);
+ }
+ throw new BadOpcodeException("Invalid opcode '{$opcode}' provided");
+ }
+}
diff --git a/vendor/textalk/websocket/lib/Message/Message.php b/vendor/textalk/websocket/lib/Message/Message.php
new file mode 100644
index 0000000..998caa1
--- /dev/null
+++ b/vendor/textalk/websocket/lib/Message/Message.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace WebSocket\Message;
+
+use DateTime;
+
+abstract class Message
+{
+ protected $opcode;
+ protected $payload;
+ protected $timestamp;
+
+ public function __construct(string $payload = '')
+ {
+ $this->payload = $payload;
+ $this->timestamp = new DateTime();
+ }
+
+ public function getOpcode(): string
+ {
+ return $this->opcode;
+ }
+
+ public function getLength(): int
+ {
+ return strlen($this->payload);
+ }
+
+ public function getTimestamp(): DateTime
+ {
+ return $this->timestamp;
+ }
+
+ public function getContent(): string
+ {
+ return $this->payload;
+ }
+
+ public function setContent(string $payload = ''): void
+ {
+ $this->payload = $payload;
+ }
+
+ public function hasContent(): bool
+ {
+ return $this->payload != '';
+ }
+
+ public function __toString(): string
+ {
+ return get_class($this);
+ }
+}
diff --git a/vendor/textalk/websocket/lib/Message/Ping.php b/vendor/textalk/websocket/lib/Message/Ping.php
new file mode 100644
index 0000000..908d233
--- /dev/null
+++ b/vendor/textalk/websocket/lib/Message/Ping.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace WebSocket\Message;
+
+class Ping extends Message
+{
+ protected $opcode = 'ping';
+}
diff --git a/vendor/textalk/websocket/lib/Message/Pong.php b/vendor/textalk/websocket/lib/Message/Pong.php
new file mode 100644
index 0000000..76a8f9e
--- /dev/null
+++ b/vendor/textalk/websocket/lib/Message/Pong.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace WebSocket\Message;
+
+class Pong extends Message
+{
+ protected $opcode = 'pong';
+}
diff --git a/vendor/textalk/websocket/lib/Message/Text.php b/vendor/textalk/websocket/lib/Message/Text.php
new file mode 100644
index 0000000..88a22f7
--- /dev/null
+++ b/vendor/textalk/websocket/lib/Message/Text.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace WebSocket\Message;
+
+class Text extends Message
+{
+ protected $opcode = 'text';
+}
diff --git a/vendor/textalk/websocket/lib/Server.php b/vendor/textalk/websocket/lib/Server.php
new file mode 100644
index 0000000..ae9325f
--- /dev/null
+++ b/vendor/textalk/websocket/lib/Server.php
@@ -0,0 +1,176 @@
+<?php
+
+/**
+ * Copyright (C) 2014-2020 Textalk/Abicart and contributors.
+ *
+ * This file is part of Websocket PHP and is free software under the ISC License.
+ * License text: https://raw.githubusercontent.com/Textalk/websocket-php/master/COPYING
+ */
+
+namespace WebSocket;
+
+class Server extends Base
+{
+ // Default options
+ protected static $default_options = [
+ 'filter' => ['text', 'binary'],
+ 'fragment_size' => 4096,
+ 'logger' => null,
+ 'port' => 8000,
+ 'return_obj' => false,
+ 'timeout' => null,
+ ];
+
+ protected $addr;
+ protected $port;
+ protected $listening;
+ protected $request;
+ protected $request_path;
+
+ /**
+ * @param array $options
+ * Associative array containing:
+ * - timeout: Set the socket timeout in seconds.
+ * - fragment_size: Set framgemnt size. Default: 4096
+ * - port: Chose port for listening. Default 8000.
+ */
+ public function __construct(array $options = [])
+ {
+ $this->options = array_merge(self::$default_options, $options);
+ $this->port = $this->options['port'];
+ $this->setLogger($this->options['logger']);
+
+ $error = $errno = $errstr = null;
+ set_error_handler(function (int $severity, string $message, string $file, int $line) use (&$error) {
+ $this->logger->warning($message, ['severity' => $severity]);
+ $error = $message;
+ }, E_ALL);
+
+ do {
+ $this->listening = stream_socket_server("tcp://0.0.0.0:$this->port", $errno, $errstr);
+ } while ($this->listening === false && $this->port++ < 10000);
+
+ restore_error_handler();
+
+ if (!$this->listening) {
+ $error = "Could not open listening socket: {$errstr} ({$errno}) {$error}";
+ $this->logger->error($error);
+ throw new ConnectionException($error, (int)$errno);
+ }
+
+ $this->logger->info("Server listening to port {$this->port}");
+ }
+
+ public function __destruct()
+ {
+ if ($this->isConnected()) {
+ fclose($this->socket);
+ }
+ $this->socket = null;
+ }
+
+ public function getPort(): int
+ {
+ return $this->port;
+ }
+
+ public function getPath(): string
+ {
+ return $this->request_path;
+ }
+
+ public function getRequest(): array
+ {
+ return $this->request;
+ }
+
+ public function getHeader($header): ?string
+ {
+ foreach ($this->request as $row) {
+ if (stripos($row, $header) !== false) {
+ list($headername, $headervalue) = explode(":", $row);
+ return trim($headervalue);
+ }
+ }
+ return null;
+ }
+
+ public function accept(): bool
+ {
+ $this->socket = null;
+ return (bool)$this->listening;
+ }
+
+ protected function connect(): void
+ {
+
+ $error = null;
+ set_error_handler(function (int $severity, string $message, string $file, int $line) use (&$error) {
+ $this->logger->warning($message, ['severity' => $severity]);
+ $error = $message;
+ }, E_ALL);
+
+ if (isset($this->options['timeout'])) {
+ $this->socket = stream_socket_accept($this->listening, $this->options['timeout']);
+ } else {
+ $this->socket = stream_socket_accept($this->listening);
+ }
+
+ restore_error_handler();
+
+ if (!$this->socket) {
+ $this->throwException("Server failed to connect. {$error}");
+ }
+ if (isset($this->options['timeout'])) {
+ stream_set_timeout($this->socket, $this->options['timeout']);
+ }
+
+ $this->logger->info("Client has connected to port {port}", [
+ 'port' => $this->port,
+ 'pier' => stream_socket_get_name($this->socket, true),
+ ]);
+ $this->performHandshake();
+ }
+
+ protected function performHandshake(): void
+ {
+ $request = '';
+ do {
+ $buffer = stream_get_line($this->socket, 1024, "\r\n");
+ $request .= $buffer . "\n";
+ $metadata = stream_get_meta_data($this->socket);
+ } while (!feof($this->socket) && $metadata['unread_bytes'] > 0);
+
+ if (!preg_match('/GET (.*) HTTP\//mUi', $request, $matches)) {
+ $error = "No GET in request: {$request}";
+ $this->logger->error($error);
+ throw new ConnectionException($error);
+ }
+ $get_uri = trim($matches[1]);
+ $uri_parts = parse_url($get_uri);
+
+ $this->request = explode("\n", $request);
+ $this->request_path = $uri_parts['path'];
+ /// @todo Get query and fragment as well.
+
+ if (!preg_match('#Sec-WebSocket-Key:\s(.*)$#mUi', $request, $matches)) {
+ $error = "Client had no Key in upgrade request: {$request}";
+ $this->logger->error($error);
+ throw new ConnectionException($error);
+ }
+
+ $key = trim($matches[1]);
+
+ /// @todo Validate key length and base 64...
+ $response_key = base64_encode(pack('H*', sha1($key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')));
+
+ $header = "HTTP/1.1 101 Switching Protocols\r\n"
+ . "Upgrade: websocket\r\n"
+ . "Connection: Upgrade\r\n"
+ . "Sec-WebSocket-Accept: $response_key\r\n"
+ . "\r\n";
+
+ $this->write($header);
+ $this->logger->debug("Handshake on {$get_uri}");
+ }
+}
diff --git a/vendor/textalk/websocket/lib/TimeoutException.php b/vendor/textalk/websocket/lib/TimeoutException.php
new file mode 100644
index 0000000..d20e622
--- /dev/null
+++ b/vendor/textalk/websocket/lib/TimeoutException.php
@@ -0,0 +1,7 @@
+<?php
+
+namespace WebSocket;
+
+class TimeoutException extends ConnectionException
+{
+}