From 7087f2232639beea47859c85ba8ac7a057966a00 Mon Sep 17 00:00:00 2001 From: M FIRDAUS RIAWAN Date: Wed, 10 Dec 2025 16:53:16 +0700 Subject: [PATCH] fix: improve PHP 8.4 stream timeout handling (issue #131) - Replace socket_get_status() with stream_get_meta_data() for clarity - Only throw timeout exception when timed_out=true AND no data received - Add configurable 'throw_timeout_exception' option to disable timeout checks - Update Laravel config with ROUTEROS_THROW_TIMEOUT_EXCEPTION env var Tested on PHP 8.4.15 with RouterOS 7.12.1, retrieved 72,788 records successfully. Fixes: https://github.com/EvilFreelancer/routeros-api-php/issues/131 --- configs/routeros-api.php | 44 ++++++++++++++--------- src/Client.php | 4 ++- src/Config.php | 64 +++++++++++++++++++--------------- src/Streams/ResourceStream.php | 29 +++++++++++++-- 4 files changed, 93 insertions(+), 48 deletions(-) diff --git a/configs/routeros-api.php b/configs/routeros-api.php index 6e3e3e8..a274f87 100644 --- a/configs/routeros-api.php +++ b/configs/routeros-api.php @@ -14,10 +14,10 @@ | */ - 'host' => env('ROUTEROS_HOST', '192.168.88.1'), // Address of Mikrotik RouterOS - 'user' => env('ROUTEROS_USER', 'admin'), // Username - 'pass' => env('ROUTEROS_PASS'), // Password - 'port' => (int) env('ROUTEROS_PORT', 8728), // RouterOS API port number for access (if not set use default or default with SSL if SSL enabled) + 'host' => env('ROUTEROS_HOST', '192.168.88.1'), // Address of Mikrotik RouterOS + 'user' => env('ROUTEROS_USER', 'admin'), // Username + 'pass' => env('ROUTEROS_PASS'), // Password + 'port' => (int) env('ROUTEROS_PORT', 8728), // RouterOS API port number for access (if not set use default or default with SSL if SSL enabled) /* |-------------------------------------------------------------------------- @@ -30,14 +30,14 @@ | */ - 'attempts' => (int) env('ROUTEROS_ATTEMPTS', 10), // Count of attempts to establish TCP session - 'delay' => (int) env('ROUTEROS_DELAY', 1), // Delay between attempts in seconds - 'timeout' => (int) env('ROUTEROS_TIMEOUT', 10), // Max timeout for instantiating connection with RouterOS - 'socket_timeout' => (int) env('ROUTEROS_SOCKET_TIMEOUT', env('ROUTEROS_TIMEOUT', 30)), // Max timeout for read from RouterOS - 'socket_blocking' => (bool) env('ROUTEROS_SOCKET_BLOCKING', true), // Set blocking mode on a socket stream + 'attempts' => (int) env('ROUTEROS_ATTEMPTS', 10), // Count of attempts to establish TCP session + 'delay' => (int) env('ROUTEROS_DELAY', 1), // Delay between attempts in seconds + 'timeout' => (int) env('ROUTEROS_TIMEOUT', 10), // Max timeout for instantiating connection with RouterOS + 'socket_timeout' => (int) env('ROUTEROS_SOCKET_TIMEOUT', env('ROUTEROS_TIMEOUT', 30)), // Max timeout for read from RouterOS + 'socket_blocking' => (bool) env('ROUTEROS_SOCKET_BLOCKING', true), // Set blocking mode on a socket stream // @see https://www.php.net/manual/en/context.socket.php - 'socket_options' => [ + 'socket_options' => [ // Examples: // 'bindto' => '192.168.0.100:0', // connect to the internet using the '192.168.0.100' IP // 'bindto' => '192.168.0.100:7000', // connect to the internet using the '192.168.0.100' IP and port '7000' @@ -61,10 +61,10 @@ | */ - 'ssl' => (bool) env('ROUTEROS_SSL', false), // Enable ssl support (if port is not set this parameter must change default port to ssl port) + 'ssl' => (bool) env('ROUTEROS_SSL', false), // Enable ssl support (if port is not set this parameter must change default port to ssl port) // @see https://www.php.net/manual/en/context.ssl.php - 'ssl_options' => [ + 'ssl_options' => [ 'ciphers' => env('ROUTEROS_SSL_CIPHERS', 'ADH:ALL'), // ADH:ALL, ADH:ALL@SECLEVEL=0, ADH:ALL@SECLEVEL=1 ... ADH:ALL@SECLEVEL=5 'verify_peer' => (bool) env('ROUTEROS_SSL_VERIFY_PEER', false), // Require verification of SSL certificate used. 'verify_peer_name' => (bool) env('ROUTEROS_SSL_VERIFY_PEER_NAME', false), // Require verification of peer name. @@ -81,9 +81,9 @@ | */ - 'ssh_port' => (int) env('ROUTEROS_SSH_PORT', 22), // Number of SSH port - 'ssh_timeout' => (int) env('ROUTEROS_SSH_TIMEOUT', env('ROUTEROS_TIMEOUT', 30)), // Max timeout for read from RouterOS via SSH proto (for "/export" command) - 'ssh_private_key' => env('ROUTEROS_SSH_PRIVKEY', '~/.ssh/id_rsa.pub'), // Full path to required private key + 'ssh_port' => (int) env('ROUTEROS_SSH_PORT', 22), // Number of SSH port + 'ssh_timeout' => (int) env('ROUTEROS_SSH_TIMEOUT', env('ROUTEROS_TIMEOUT', 30)), // Max timeout for read from RouterOS via SSH proto (for "/export" command) + 'ssh_private_key' => env('ROUTEROS_SSH_PRIVKEY', '~/.ssh/id_rsa.pub'), // Full path to required private key /* |-------------------------------------------------------------------------- @@ -95,6 +95,18 @@ | */ - 'legacy' => (bool) env('ROUTEROS_LEGACY', false), // Support of legacy login scheme (true - pre 6.43, false - post 6.43) + 'legacy' => (bool) env('ROUTEROS_LEGACY', false), // Support of legacy login scheme (true - pre 6.43, false - post 6.43) + + /* + |-------------------------------------------------------------------------- + | PHP 8.4 Compatibility + |-------------------------------------------------------------------------- + | + | If you experience "Stream timed out" errors on PHP 8.4, you can disable + | the timeout exception by setting this to false. + | + */ + + 'throw_timeout_exception' => (bool) env('ROUTEROS_THROW_TIMEOUT_EXCEPTION', true), // Throw exception on stream timeout ]; diff --git a/src/Client.php b/src/Client.php index a8ce67f..ee8152d 100644 --- a/src/Client.php +++ b/src/Client.php @@ -540,7 +540,9 @@ public function connect(): bool // If socket is active if (null !== $this->getSocket()) { - $this->connector = new APIConnector(new Streams\ResourceStream($this->getSocket())); + $stream = new Streams\ResourceStream($this->getSocket()); + $stream->setThrowTimeoutException($this->config('throw_timeout_exception')); + $this->connector = new APIConnector($stream); // If we logged in then exit from loop if (true === $this->login()) { $connected = true; diff --git a/src/Config.php b/src/Config.php index fdbe089..2b6cac1 100644 --- a/src/Config.php +++ b/src/Config.php @@ -122,26 +122,33 @@ class Config implements ConfigInterface public const SSH_PRIVATE_KEY = '~/.ssh/id_rsa'; + /** + * By default throw exception on stream timeout + * Set to false to disable timeout exception (useful for PHP 8.4 compatibility issues) + */ + public const THROW_TIMEOUT_EXCEPTION = true; + /** * List of allowed parameters of config */ public const ALLOWED = [ - 'host' => 'string', // Address of Mikrotik RouterOS - 'user' => 'string', // Username - 'pass' => 'string', // Password - 'port' => 'integer', // RouterOS API port number for access (if not set use default or default with SSL if SSL enabled) - 'ssl' => 'boolean', // Enable ssl support (if port is not set this parameter must change default port to ssl port) - 'ssl_options' => 'array', // List of SSL options, eg. - 'legacy' => 'boolean', // Support of legacy login scheme (true - pre 6.43, false - post 6.43) - 'timeout' => 'integer', // Max timeout for instantiating connection with RouterOS - 'socket_timeout' => 'integer', // Max timeout for read from RouterOS - 'socket_blocking' => 'boolean', // Set blocking mode on a socket stream - 'socket_options' => 'array', // List of socket context options - 'attempts' => 'integer', // Count of attempts to establish TCP session - 'delay' => 'integer', // Delay between attempts in seconds - 'ssh_port' => 'integer', // Number of SSH port - 'ssh_timeout' => 'integer', // Max timeout for read from RouterOS via SSH proto (for "/export" command) - 'ssh_private_key' => 'string', // Max timeout for read from RouterOS via SSH proto (for "/export" command) + 'host' => 'string', // Address of Mikrotik RouterOS + 'user' => 'string', // Username + 'pass' => 'string', // Password + 'port' => 'integer', // RouterOS API port number for access (if not set use default or default with SSL if SSL enabled) + 'ssl' => 'boolean', // Enable ssl support (if port is not set this parameter must change default port to ssl port) + 'ssl_options' => 'array', // List of SSL options, eg. + 'legacy' => 'boolean', // Support of legacy login scheme (true - pre 6.43, false - post 6.43) + 'timeout' => 'integer', // Max timeout for instantiating connection with RouterOS + 'socket_timeout' => 'integer', // Max timeout for read from RouterOS + 'socket_blocking' => 'boolean', // Set blocking mode on a socket stream + 'socket_options' => 'array', // List of socket context options + 'attempts' => 'integer', // Count of attempts to establish TCP session + 'delay' => 'integer', // Delay between attempts in seconds + 'ssh_port' => 'integer', // Number of SSH port + 'ssh_timeout' => 'integer', // Max timeout for read from RouterOS via SSH proto (for "/export" command) + 'ssh_private_key' => 'string', // Full path to required private key + 'throw_timeout_exception' => 'boolean', // Throw exception on stream timeout (set false to disable for PHP 8.4 issues) ]; /** @@ -150,18 +157,19 @@ class Config implements ConfigInterface * @var array */ private $_parameters = [ - 'legacy' => self::LEGACY, - 'ssl' => self::SSL, - 'ssl_options' => self::SSL_OPTIONS, - 'timeout' => self::TIMEOUT, - 'socket_timeout' => self::SOCKET_TIMEOUT, - 'socket_blocking' => self::SOCKET_BLOCKING, - 'socket_options' => self::SOCKET_OPTIONS, - 'attempts' => self::ATTEMPTS, - 'delay' => self::ATTEMPTS_DELAY, - 'ssh_port' => self::SSH_PORT, - 'ssh_timeout' => self::SSH_TIMEOUT, - 'ssh_private_key' => self::SSH_PRIVATE_KEY, + 'legacy' => self::LEGACY, + 'ssl' => self::SSL, + 'ssl_options' => self::SSL_OPTIONS, + 'timeout' => self::TIMEOUT, + 'socket_timeout' => self::SOCKET_TIMEOUT, + 'socket_blocking' => self::SOCKET_BLOCKING, + 'socket_options' => self::SOCKET_OPTIONS, + 'attempts' => self::ATTEMPTS, + 'delay' => self::ATTEMPTS_DELAY, + 'ssh_port' => self::SSH_PORT, + 'ssh_timeout' => self::SSH_TIMEOUT, + 'ssh_private_key' => self::SSH_PRIVATE_KEY, + 'throw_timeout_exception' => self::THROW_TIMEOUT_EXCEPTION, ]; /** diff --git a/src/Streams/ResourceStream.php b/src/Streams/ResourceStream.php index 32b6865..ba0183c 100644 --- a/src/Streams/ResourceStream.php +++ b/src/Streams/ResourceStream.php @@ -17,6 +17,13 @@ class ResourceStream implements StreamInterface { protected $stream; + /** + * Whether to throw exception on stream timeout + * + * @var bool + */ + protected $throwTimeoutException = true; + /** * ResourceStream constructor. * @@ -32,6 +39,18 @@ public function __construct($stream) $this->stream = $stream; } + /** + * Set whether to throw exception on stream timeout + * + * @param bool $throw + * @return self + */ + public function setThrowTimeoutException(bool $throw): self + { + $this->throwTimeoutException = $throw; + return $this; + } + /** * {@inheritDoc} * @@ -50,9 +69,13 @@ public function read(int $length): string $result = fread($this->stream, $length); - // Stream in blocking mode timed out - if(socket_get_status($this->stream)['timed_out']){ - throw new StreamException('Stream timed out'); + // PHP 8.4 may report timed_out=true even on successful partial reads + // Only throw timeout if we got no data AND stream actually timed out + if ($this->throwTimeoutException) { + $info = stream_get_meta_data($this->stream); + if ($info['timed_out'] && ($result === '' || $result === false)) { + throw new StreamException('Stream timed out'); + } } if (false === $result) {