Skip to content

Commit ac697bd

Browse files
Merge pull request #140 from sonarta/fix/issue-131-php84-timeout
fix: improve PHP 8.4 stream timeout handling (issue #131)
2 parents 7cae422 + 7087f22 commit ac697bd

4 files changed

Lines changed: 93 additions & 48 deletions

File tree

configs/routeros-api.php

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@
1414
|
1515
*/
1616

17-
'host' => env('ROUTEROS_HOST', '192.168.88.1'), // Address of Mikrotik RouterOS
18-
'user' => env('ROUTEROS_USER', 'admin'), // Username
19-
'pass' => env('ROUTEROS_PASS'), // Password
20-
'port' => (int) env('ROUTEROS_PORT', 8728), // RouterOS API port number for access (if not set use default or default with SSL if SSL enabled)
17+
'host' => env('ROUTEROS_HOST', '192.168.88.1'), // Address of Mikrotik RouterOS
18+
'user' => env('ROUTEROS_USER', 'admin'), // Username
19+
'pass' => env('ROUTEROS_PASS'), // Password
20+
'port' => (int) env('ROUTEROS_PORT', 8728), // RouterOS API port number for access (if not set use default or default with SSL if SSL enabled)
2121

2222
/*
2323
|--------------------------------------------------------------------------
@@ -30,14 +30,14 @@
3030
|
3131
*/
3232

33-
'attempts' => (int) env('ROUTEROS_ATTEMPTS', 10), // Count of attempts to establish TCP session
34-
'delay' => (int) env('ROUTEROS_DELAY', 1), // Delay between attempts in seconds
35-
'timeout' => (int) env('ROUTEROS_TIMEOUT', 10), // Max timeout for instantiating connection with RouterOS
36-
'socket_timeout' => (int) env('ROUTEROS_SOCKET_TIMEOUT', env('ROUTEROS_TIMEOUT', 30)), // Max timeout for read from RouterOS
37-
'socket_blocking' => (bool) env('ROUTEROS_SOCKET_BLOCKING', true), // Set blocking mode on a socket stream
33+
'attempts' => (int) env('ROUTEROS_ATTEMPTS', 10), // Count of attempts to establish TCP session
34+
'delay' => (int) env('ROUTEROS_DELAY', 1), // Delay between attempts in seconds
35+
'timeout' => (int) env('ROUTEROS_TIMEOUT', 10), // Max timeout for instantiating connection with RouterOS
36+
'socket_timeout' => (int) env('ROUTEROS_SOCKET_TIMEOUT', env('ROUTEROS_TIMEOUT', 30)), // Max timeout for read from RouterOS
37+
'socket_blocking' => (bool) env('ROUTEROS_SOCKET_BLOCKING', true), // Set blocking mode on a socket stream
3838

3939
// @see https://www.php.net/manual/en/context.socket.php
40-
'socket_options' => [
40+
'socket_options' => [
4141
// Examples:
4242
// 'bindto' => '192.168.0.100:0', // connect to the internet using the '192.168.0.100' IP
4343
// 'bindto' => '192.168.0.100:7000', // connect to the internet using the '192.168.0.100' IP and port '7000'
@@ -61,10 +61,10 @@
6161
|
6262
*/
6363

64-
'ssl' => (bool) env('ROUTEROS_SSL', false), // Enable ssl support (if port is not set this parameter must change default port to ssl port)
64+
'ssl' => (bool) env('ROUTEROS_SSL', false), // Enable ssl support (if port is not set this parameter must change default port to ssl port)
6565

6666
// @see https://www.php.net/manual/en/context.ssl.php
67-
'ssl_options' => [
67+
'ssl_options' => [
6868
'ciphers' => env('ROUTEROS_SSL_CIPHERS', 'ADH:ALL'), // ADH:ALL, ADH:ALL@SECLEVEL=0, ADH:ALL@SECLEVEL=1 ... ADH:ALL@SECLEVEL=5
6969
'verify_peer' => (bool) env('ROUTEROS_SSL_VERIFY_PEER', false), // Require verification of SSL certificate used.
7070
'verify_peer_name' => (bool) env('ROUTEROS_SSL_VERIFY_PEER_NAME', false), // Require verification of peer name.
@@ -81,9 +81,9 @@
8181
|
8282
*/
8383

84-
'ssh_port' => (int) env('ROUTEROS_SSH_PORT', 22), // Number of SSH port
85-
'ssh_timeout' => (int) env('ROUTEROS_SSH_TIMEOUT', env('ROUTEROS_TIMEOUT', 30)), // Max timeout for read from RouterOS via SSH proto (for "/export" command)
86-
'ssh_private_key' => env('ROUTEROS_SSH_PRIVKEY', '~/.ssh/id_rsa.pub'), // Full path to required private key
84+
'ssh_port' => (int) env('ROUTEROS_SSH_PORT', 22), // Number of SSH port
85+
'ssh_timeout' => (int) env('ROUTEROS_SSH_TIMEOUT', env('ROUTEROS_TIMEOUT', 30)), // Max timeout for read from RouterOS via SSH proto (for "/export" command)
86+
'ssh_private_key' => env('ROUTEROS_SSH_PRIVKEY', '~/.ssh/id_rsa.pub'), // Full path to required private key
8787

8888
/*
8989
|--------------------------------------------------------------------------
@@ -95,6 +95,18 @@
9595
|
9696
*/
9797

98-
'legacy' => (bool) env('ROUTEROS_LEGACY', false), // Support of legacy login scheme (true - pre 6.43, false - post 6.43)
98+
'legacy' => (bool) env('ROUTEROS_LEGACY', false), // Support of legacy login scheme (true - pre 6.43, false - post 6.43)
99+
100+
/*
101+
|--------------------------------------------------------------------------
102+
| PHP 8.4 Compatibility
103+
|--------------------------------------------------------------------------
104+
|
105+
| If you experience "Stream timed out" errors on PHP 8.4, you can disable
106+
| the timeout exception by setting this to false.
107+
|
108+
*/
109+
110+
'throw_timeout_exception' => (bool) env('ROUTEROS_THROW_TIMEOUT_EXCEPTION', true), // Throw exception on stream timeout
99111

100112
];

src/Client.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -540,7 +540,9 @@ public function connect(): bool
540540

541541
// If socket is active
542542
if (null !== $this->getSocket()) {
543-
$this->connector = new APIConnector(new Streams\ResourceStream($this->getSocket()));
543+
$stream = new Streams\ResourceStream($this->getSocket());
544+
$stream->setThrowTimeoutException($this->config('throw_timeout_exception'));
545+
$this->connector = new APIConnector($stream);
544546
// If we logged in then exit from loop
545547
if (true === $this->login()) {
546548
$connected = true;

src/Config.php

Lines changed: 36 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -122,26 +122,33 @@ class Config implements ConfigInterface
122122

123123
public const SSH_PRIVATE_KEY = '~/.ssh/id_rsa';
124124

125+
/**
126+
* By default throw exception on stream timeout
127+
* Set to false to disable timeout exception (useful for PHP 8.4 compatibility issues)
128+
*/
129+
public const THROW_TIMEOUT_EXCEPTION = true;
130+
125131
/**
126132
* List of allowed parameters of config
127133
*/
128134
public const ALLOWED = [
129-
'host' => 'string', // Address of Mikrotik RouterOS
130-
'user' => 'string', // Username
131-
'pass' => 'string', // Password
132-
'port' => 'integer', // RouterOS API port number for access (if not set use default or default with SSL if SSL enabled)
133-
'ssl' => 'boolean', // Enable ssl support (if port is not set this parameter must change default port to ssl port)
134-
'ssl_options' => 'array', // List of SSL options, eg.
135-
'legacy' => 'boolean', // Support of legacy login scheme (true - pre 6.43, false - post 6.43)
136-
'timeout' => 'integer', // Max timeout for instantiating connection with RouterOS
137-
'socket_timeout' => 'integer', // Max timeout for read from RouterOS
138-
'socket_blocking' => 'boolean', // Set blocking mode on a socket stream
139-
'socket_options' => 'array', // List of socket context options
140-
'attempts' => 'integer', // Count of attempts to establish TCP session
141-
'delay' => 'integer', // Delay between attempts in seconds
142-
'ssh_port' => 'integer', // Number of SSH port
143-
'ssh_timeout' => 'integer', // Max timeout for read from RouterOS via SSH proto (for "/export" command)
144-
'ssh_private_key' => 'string', // Max timeout for read from RouterOS via SSH proto (for "/export" command)
135+
'host' => 'string', // Address of Mikrotik RouterOS
136+
'user' => 'string', // Username
137+
'pass' => 'string', // Password
138+
'port' => 'integer', // RouterOS API port number for access (if not set use default or default with SSL if SSL enabled)
139+
'ssl' => 'boolean', // Enable ssl support (if port is not set this parameter must change default port to ssl port)
140+
'ssl_options' => 'array', // List of SSL options, eg.
141+
'legacy' => 'boolean', // Support of legacy login scheme (true - pre 6.43, false - post 6.43)
142+
'timeout' => 'integer', // Max timeout for instantiating connection with RouterOS
143+
'socket_timeout' => 'integer', // Max timeout for read from RouterOS
144+
'socket_blocking' => 'boolean', // Set blocking mode on a socket stream
145+
'socket_options' => 'array', // List of socket context options
146+
'attempts' => 'integer', // Count of attempts to establish TCP session
147+
'delay' => 'integer', // Delay between attempts in seconds
148+
'ssh_port' => 'integer', // Number of SSH port
149+
'ssh_timeout' => 'integer', // Max timeout for read from RouterOS via SSH proto (for "/export" command)
150+
'ssh_private_key' => 'string', // Full path to required private key
151+
'throw_timeout_exception' => 'boolean', // Throw exception on stream timeout (set false to disable for PHP 8.4 issues)
145152
];
146153

147154
/**
@@ -150,18 +157,19 @@ class Config implements ConfigInterface
150157
* @var array
151158
*/
152159
private $_parameters = [
153-
'legacy' => self::LEGACY,
154-
'ssl' => self::SSL,
155-
'ssl_options' => self::SSL_OPTIONS,
156-
'timeout' => self::TIMEOUT,
157-
'socket_timeout' => self::SOCKET_TIMEOUT,
158-
'socket_blocking' => self::SOCKET_BLOCKING,
159-
'socket_options' => self::SOCKET_OPTIONS,
160-
'attempts' => self::ATTEMPTS,
161-
'delay' => self::ATTEMPTS_DELAY,
162-
'ssh_port' => self::SSH_PORT,
163-
'ssh_timeout' => self::SSH_TIMEOUT,
164-
'ssh_private_key' => self::SSH_PRIVATE_KEY,
160+
'legacy' => self::LEGACY,
161+
'ssl' => self::SSL,
162+
'ssl_options' => self::SSL_OPTIONS,
163+
'timeout' => self::TIMEOUT,
164+
'socket_timeout' => self::SOCKET_TIMEOUT,
165+
'socket_blocking' => self::SOCKET_BLOCKING,
166+
'socket_options' => self::SOCKET_OPTIONS,
167+
'attempts' => self::ATTEMPTS,
168+
'delay' => self::ATTEMPTS_DELAY,
169+
'ssh_port' => self::SSH_PORT,
170+
'ssh_timeout' => self::SSH_TIMEOUT,
171+
'ssh_private_key' => self::SSH_PRIVATE_KEY,
172+
'throw_timeout_exception' => self::THROW_TIMEOUT_EXCEPTION,
165173
];
166174

167175
/**

src/Streams/ResourceStream.php

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@ class ResourceStream implements StreamInterface
1717
{
1818
protected $stream;
1919

20+
/**
21+
* Whether to throw exception on stream timeout
22+
*
23+
* @var bool
24+
*/
25+
protected $throwTimeoutException = true;
26+
2027
/**
2128
* ResourceStream constructor.
2229
*
@@ -32,6 +39,18 @@ public function __construct($stream)
3239
$this->stream = $stream;
3340
}
3441

42+
/**
43+
* Set whether to throw exception on stream timeout
44+
*
45+
* @param bool $throw
46+
* @return self
47+
*/
48+
public function setThrowTimeoutException(bool $throw): self
49+
{
50+
$this->throwTimeoutException = $throw;
51+
return $this;
52+
}
53+
3554
/**
3655
* {@inheritDoc}
3756
*
@@ -50,9 +69,13 @@ public function read(int $length): string
5069

5170
$result = fread($this->stream, $length);
5271

53-
// Stream in blocking mode timed out
54-
if(socket_get_status($this->stream)['timed_out']){
55-
throw new StreamException('Stream timed out');
72+
// PHP 8.4 may report timed_out=true even on successful partial reads
73+
// Only throw timeout if we got no data AND stream actually timed out
74+
if ($this->throwTimeoutException) {
75+
$info = stream_get_meta_data($this->stream);
76+
if ($info['timed_out'] && ($result === '' || $result === false)) {
77+
throw new StreamException('Stream timed out');
78+
}
5679
}
5780

5881
if (false === $result) {

0 commit comments

Comments
 (0)