1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace PjbServer\Tools\Network; |
4
|
|
|
|
5
|
|
|
class PortTester |
6
|
|
|
{ |
7
|
|
|
|
8
|
|
|
const BACKEND_STREAM_SOCKET = 'stream_socket'; |
9
|
|
|
const BACKEND_SOCKET_CREATE = 'socket_create'; |
10
|
|
|
const BACKEND_PFSOCKOPEN = 'pfsockopen'; |
11
|
|
|
const BACKEND_CURL = 'curl'; |
12
|
|
|
|
13
|
|
|
|
14
|
|
|
const PROTOCOL_TCP = 'tcp'; |
15
|
|
|
const PROTOCOL_UDP = 'udp'; |
16
|
|
|
const PROTOCOL_HTTP = 'http'; |
17
|
|
|
const PROTOCOL_HTTPS = 'https'; |
18
|
|
|
|
19
|
|
|
/** |
20
|
|
|
* @var array |
21
|
|
|
*/ |
22
|
|
|
protected $supportedBackends = array('stream_socket', 'socket_create', 'pfsockopen', 'curl'); |
23
|
|
|
|
24
|
|
|
/** |
25
|
|
|
* @var array |
26
|
|
|
*/ |
27
|
|
|
protected $supportedProtocols = array('tcp', 'udp', 'http', 'https'); |
28
|
|
|
|
29
|
|
|
|
30
|
|
|
/** |
31
|
|
|
* @var array |
32
|
|
|
*/ |
33
|
|
|
protected $defaults = array( |
34
|
|
|
'backend' => null, |
35
|
|
|
'timeout' => 1, |
36
|
|
|
'close_timeout_ms' => null |
37
|
|
|
); |
38
|
|
|
|
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* |
42
|
|
|
* @var array |
43
|
|
|
*/ |
44
|
|
|
protected $options; |
45
|
|
|
|
46
|
|
|
/** |
47
|
|
|
* Constructor |
48
|
|
|
* |
49
|
|
|
* <code> |
50
|
|
|
* $options = [ |
51
|
|
|
* 'backend' => PortTester::BACKEND_STREAM_SOCKET, |
52
|
|
|
* // connection timeout in seconds |
53
|
|
|
* 'timeout' => 1, |
54
|
|
|
* // timeout to wait for connection to be closed |
55
|
|
|
* // properly in milliseconds or null to disable |
56
|
|
|
* // Use when TIME_WAIT is too long |
57
|
|
|
* 'close_timeout_ms => 300 |
58
|
|
|
* ]; |
59
|
|
|
* $portTester = new PortTester($options); |
60
|
|
|
* </code> |
61
|
|
|
* |
62
|
|
|
* @throws \InvalidArgumentException |
63
|
|
|
* @param array $options |
64
|
|
|
*/ |
65
|
5 |
|
public function __construct($options = array()) |
66
|
|
|
{ |
67
|
5 |
|
$this->options = array_merge($this->defaults, $options); |
68
|
5 |
|
if ($this->options['backend'] == '') { |
69
|
|
|
if ($this->isCurlAvailable()) { |
70
|
|
|
$this->options['backend'] = self::BACKEND_CURL; |
71
|
|
|
} else { |
72
|
|
|
$this->options['backend'] = self::BACKEND_STREAM_SOCKET; |
73
|
|
|
} |
74
|
|
|
} |
75
|
5 |
|
if (!in_array($this->options['backend'], $this->supportedBackends)) { |
76
|
|
|
throw new \InvalidArgumentException("Unsupported backend '" . $this->options['backend'] . "'"); |
77
|
|
|
} |
78
|
5 |
|
} |
79
|
|
|
|
80
|
|
|
/** |
81
|
|
|
* Check if TCP port is available for binding |
82
|
|
|
* |
83
|
|
|
* @throws \InvalidArgumentException |
84
|
|
|
* @throws \RuntimeException |
85
|
|
|
* |
86
|
|
|
* @param string $host |
87
|
|
|
* @param int $port |
88
|
|
|
* @param string $protocol |
89
|
|
|
* @param int|null $timeout |
90
|
|
|
* @return boolean |
91
|
|
|
*/ |
92
|
3 |
|
public function isAvailable($host, $port, $protocol = 'http', $timeout = null) |
93
|
|
|
{ |
94
|
3 |
|
if (!in_array($protocol, $this->supportedProtocols)) { |
95
|
|
|
throw new \InvalidArgumentException("Unsupported protocol '$protocol'"); |
96
|
|
|
} |
97
|
|
|
|
98
|
3 |
|
if ($timeout === null) { |
99
|
3 |
|
$timeout = $this->options['timeout']; |
100
|
3 |
|
} |
101
|
|
|
|
102
|
3 |
|
$available = false; |
103
|
3 |
|
$sock = null; |
104
|
|
|
|
105
|
3 |
|
$backend = $this->options['backend']; |
106
|
|
|
switch ($backend) { |
107
|
3 |
|
case self::BACKEND_PFSOCKOPEN: |
108
|
|
|
$sock = @pfsockopen("$protocol://$host", $port, $errno, $errstr, $timeout); |
109
|
|
|
if (!$sock) { |
110
|
|
|
$available = true; |
111
|
|
|
} else { |
112
|
|
|
fclose($sock); |
113
|
|
|
} |
114
|
|
|
break; |
115
|
|
|
|
116
|
3 |
|
case self::BACKEND_SOCKET_CREATE: |
117
|
|
|
$timeout = 0; |
118
|
|
|
$protocolMap = array('tcp' => SOL_TCP, 'udp' => SOL_UDP); |
119
|
|
|
if (!array_key_exists($protocol, $protocolMap)) { |
120
|
|
|
throw new \RuntimeException("Backedn socket_create does not support protocol $protocol"); |
121
|
|
|
} |
122
|
|
|
$proto = $protocolMap[$protocol]; |
123
|
|
|
|
124
|
|
|
$sock = socket_create(AF_INET, SOCK_STREAM, $proto); |
125
|
|
|
socket_set_option($sock, SOL_SOCKET, SO_RCVTIMEO, array('sec' => $timeout, 'usec' => 0)); |
126
|
|
|
if (!@socket_connect($sock, $host, $port)) { |
127
|
|
|
$available = true; |
128
|
|
|
} else { |
129
|
|
|
socket_close($sock); |
130
|
|
|
} |
131
|
|
|
break; |
132
|
|
|
|
133
|
3 |
|
case self::BACKEND_STREAM_SOCKET: |
134
|
|
|
$flags = STREAM_CLIENT_CONNECT; // & ~STREAM_CLIENT_PERSISTENT |
135
|
|
|
$sock = @stream_socket_client("$protocol://$host:$port", $errno, $errstr, $timeout, $flags); |
136
|
|
|
if (!$sock) { |
137
|
|
|
$available = true; |
138
|
|
|
} else { |
139
|
|
|
if (!stream_socket_shutdown($sock, STREAM_SHUT_RDWR)) { |
140
|
|
|
throw new \RuntimeException("Cannot properly close socket stream."); |
141
|
|
|
} |
142
|
|
|
fclose($sock); |
143
|
|
|
//fclose($sock); |
144
|
|
|
//unset($sock); |
145
|
|
|
} |
146
|
|
|
break; |
147
|
|
|
|
148
|
3 |
|
case 'curl': |
149
|
3 |
|
if (!$this->isCurlAvailable()) { |
150
|
|
|
throw new \RuntimeException("Curl not available"); |
151
|
|
|
} |
152
|
|
|
$curl_options = array( |
153
|
3 |
|
CURLOPT_URL => "http://$host:$port", |
154
|
3 |
|
CURLOPT_TIMEOUT => $timeout, |
155
|
3 |
|
CURLOPT_RETURNTRANSFER => true, |
156
|
3 |
|
CURLOPT_FAILONERROR => true, |
157
|
3 |
|
CURLOPT_PORT => $port, |
158
|
3 |
|
); |
159
|
|
|
|
160
|
3 |
|
$curl_handle = curl_init(); |
161
|
3 |
|
curl_setopt_array($curl_handle, $curl_options); |
162
|
3 |
|
curl_exec($curl_handle); |
163
|
3 |
|
$errno = curl_errno($curl_handle); |
164
|
3 |
|
if ($errno != 0) { |
165
|
3 |
|
$available = true; |
166
|
|
|
//echo curl_error($curl_handle); |
|
|
|
|
167
|
3 |
|
} |
168
|
3 |
|
curl_close($curl_handle); |
169
|
3 |
|
break; |
170
|
|
|
default: |
171
|
|
|
throw new \InvalidArgumentException("Unsupported backend: '$backend'."); |
172
|
|
|
} |
173
|
3 |
|
unset($sock); |
174
|
3 |
|
if ($this->options['close_timeout_ms'] > 0) { |
175
|
|
|
usleep($this->options['close_timeout_ms'] * 1000); |
176
|
|
|
} |
177
|
3 |
|
return $available; |
178
|
|
|
} |
179
|
|
|
|
180
|
|
|
|
181
|
|
|
/** |
182
|
|
|
* Test whether curl extension is available |
183
|
|
|
* @return boolean |
184
|
|
|
*/ |
185
|
3 |
|
protected function isCurlAvailable() |
186
|
|
|
{ |
187
|
3 |
|
return function_exists('curl_version'); |
188
|
|
|
} |
189
|
|
|
} |
190
|
|
|
|
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.
The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.
This check looks for comments that seem to be mostly valid code and reports them.