From fcbf3ce37ca8f90a3e36d524a3274ffc063a40e3 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 14 Apr 2024 15:26:02 +0200 Subject: Adding upstream version 0.10.2+dfsg1. Signed-off-by: Daniel Baumann --- .../textalk/websocket/lib/BadOpcodeException.php | 7 + vendor/textalk/websocket/lib/BadUriException.php | 7 + vendor/textalk/websocket/lib/Base.php | 486 +++++++++++++++++++++ vendor/textalk/websocket/lib/Client.php | 216 +++++++++ .../textalk/websocket/lib/ConnectionException.php | 26 ++ vendor/textalk/websocket/lib/Exception.php | 7 + vendor/textalk/websocket/lib/Message/Binary.php | 8 + vendor/textalk/websocket/lib/Message/Close.php | 8 + vendor/textalk/websocket/lib/Message/Factory.php | 25 ++ vendor/textalk/websocket/lib/Message/Message.php | 53 +++ vendor/textalk/websocket/lib/Message/Ping.php | 8 + vendor/textalk/websocket/lib/Message/Pong.php | 8 + vendor/textalk/websocket/lib/Message/Text.php | 8 + vendor/textalk/websocket/lib/Server.php | 176 ++++++++ vendor/textalk/websocket/lib/TimeoutException.php | 7 + 15 files changed, 1050 insertions(+) create mode 100644 vendor/textalk/websocket/lib/BadOpcodeException.php create mode 100644 vendor/textalk/websocket/lib/BadUriException.php create mode 100644 vendor/textalk/websocket/lib/Base.php create mode 100644 vendor/textalk/websocket/lib/Client.php create mode 100644 vendor/textalk/websocket/lib/ConnectionException.php create mode 100644 vendor/textalk/websocket/lib/Exception.php create mode 100644 vendor/textalk/websocket/lib/Message/Binary.php create mode 100644 vendor/textalk/websocket/lib/Message/Close.php create mode 100644 vendor/textalk/websocket/lib/Message/Factory.php create mode 100644 vendor/textalk/websocket/lib/Message/Message.php create mode 100644 vendor/textalk/websocket/lib/Message/Ping.php create mode 100644 vendor/textalk/websocket/lib/Message/Pong.php create mode 100644 vendor/textalk/websocket/lib/Message/Text.php create mode 100644 vendor/textalk/websocket/lib/Server.php create mode 100644 vendor/textalk/websocket/lib/TimeoutException.php (limited to 'vendor/textalk/websocket/lib') 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 @@ + 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 @@ + 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 @@ +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 @@ +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 @@ + ['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 @@ +