1 | <?php |
||
28 | class Server |
||
29 | { |
||
30 | |||
31 | /* |
||
32 | * Server array keys |
||
33 | */ |
||
34 | const SERVER_TYPE = 'type'; |
||
35 | |||
36 | const SERVER_HOST = 'host'; |
||
37 | |||
38 | const SERVER_ID = 'id'; |
||
39 | |||
40 | const SERVER_OPTIONS = 'options'; |
||
41 | |||
42 | /* |
||
43 | * Server options keys |
||
44 | */ |
||
45 | |||
46 | /* |
||
47 | * Use this option when the query_port and client connect ports are different |
||
48 | */ |
||
49 | const SERVER_OPTIONS_QUERY_PORT = 'query_port'; |
||
50 | |||
51 | /** |
||
52 | * The protocol class for this server |
||
53 | * |
||
54 | * @type \GameQ\Protocol |
||
55 | */ |
||
56 | protected $protocol = null; |
||
57 | |||
58 | /** |
||
59 | * Id of this server |
||
60 | * |
||
61 | * @type string |
||
62 | */ |
||
63 | public $id = null; |
||
64 | |||
65 | /** |
||
66 | * IP Address of this server |
||
67 | * |
||
68 | * @type string |
||
69 | */ |
||
70 | public $ip = null; |
||
71 | |||
72 | /** |
||
73 | * The server's client port (connect port) |
||
74 | * |
||
75 | * @type int |
||
76 | */ |
||
77 | public $port_client = null; |
||
78 | |||
79 | /** |
||
80 | * The server's query port |
||
81 | * |
||
82 | * @type int |
||
83 | */ |
||
84 | public $port_query = null; |
||
85 | |||
86 | /** |
||
87 | * Holds other server specific options |
||
88 | * |
||
89 | * @type array |
||
90 | */ |
||
91 | protected $options = []; |
||
92 | |||
93 | /** |
||
94 | * Holds the sockets already open for this server |
||
95 | * |
||
96 | * @type array |
||
97 | */ |
||
98 | protected $sockets = []; |
||
99 | |||
100 | /** |
||
101 | * Construct the class with the passed options |
||
102 | * |
||
103 | * @param array $server_info |
||
104 | * |
||
105 | * @throws \GameQ\Exception\Server |
||
106 | */ |
||
107 | 145 | public function __construct(array $server_info = []) |
|
108 | { |
||
109 | |||
110 | // Check for server type |
||
111 | 145 | if (!array_key_exists(self::SERVER_TYPE, $server_info) || empty($server_info[self::SERVER_TYPE])) { |
|
112 | 1 | throw new Exception("Missing server info key '" . self::SERVER_TYPE . "'!"); |
|
113 | } |
||
114 | |||
115 | // Check for server host |
||
116 | 144 | if (!array_key_exists(self::SERVER_HOST, $server_info) || empty($server_info[self::SERVER_HOST])) { |
|
117 | 1 | throw new Exception("Missing server info key '" . self::SERVER_HOST . "'!"); |
|
118 | } |
||
119 | |||
120 | // IP address and port check |
||
121 | 143 | $this->checkAndSetIpPort($server_info[self::SERVER_HOST]); |
|
122 | |||
123 | // Check for server id |
||
124 | 139 | if (array_key_exists(self::SERVER_ID, $server_info) && !empty($server_info[self::SERVER_ID])) { |
|
125 | // Set the server id |
||
126 | 2 | $this->id = $server_info[self::SERVER_ID]; |
|
127 | 2 | } else { |
|
128 | // Make an id so each server has an id when returned |
||
129 | 138 | $this->id = sprintf('%s:%d', $this->ip, $this->port_client); |
|
130 | } |
||
131 | |||
132 | // Check and set server options |
||
133 | 139 | if (array_key_exists(self::SERVER_OPTIONS, $server_info)) { |
|
134 | // Set the options |
||
135 | 130 | $this->options = $server_info[self::SERVER_OPTIONS]; |
|
136 | 130 | } |
|
137 | |||
138 | try { |
||
139 | // Make the protocol class for this type |
||
140 | 139 | $class = new \ReflectionClass( |
|
141 | 139 | sprintf('GameQ\\Protocols\\%s', ucfirst(strtolower($server_info[self::SERVER_TYPE]))) |
|
142 | 139 | ); |
|
143 | |||
144 | 138 | $this->protocol = $class->newInstanceArgs([$this->options]); |
|
145 | 139 | } catch (\ReflectionException $e) { |
|
146 | 1 | throw new Exception("Unable to locate Protocols class for '{$server_info[self::SERVER_TYPE]}'!"); |
|
147 | } |
||
148 | |||
149 | // Check and set any server options |
||
150 | 138 | $this->checkAndSetServerOptions(); |
|
151 | |||
152 | 138 | unset($server_info, $class); |
|
153 | 138 | } |
|
154 | |||
155 | /** |
||
156 | * Check and set the ip address for this server |
||
157 | * |
||
158 | * @param $ip_address |
||
159 | * |
||
160 | * @throws \GameQ\Exception\Server |
||
161 | */ |
||
162 | 143 | protected function checkAndSetIpPort($ip_address) |
|
163 | { |
||
164 | |||
165 | // Test for IPv6 |
||
166 | 143 | if (substr_count($ip_address, ':') > 1) { |
|
167 | // See if we have a port, input should be in the format [::1]:27015 or similar |
||
168 | 3 | if (strstr($ip_address, ']:')) { |
|
169 | // Explode to get port |
||
170 | 2 | $server_addr = explode(':', $ip_address); |
|
171 | |||
172 | // Port is the last item in the array, remove it and save |
||
173 | 2 | $this->port_client = (int)array_pop($server_addr); |
|
174 | |||
175 | // The rest is the address, recombine |
||
176 | 2 | $this->ip = implode(':', $server_addr); |
|
177 | |||
178 | 2 | unset($server_addr); |
|
179 | 2 | } else { |
|
180 | // Just the IPv6 address, no port defined, fail |
||
181 | 1 | throw new Exception( |
|
182 | 1 | "The host address '{$ip_address}' is missing the port. All " |
|
183 | . "servers must have a port defined!" |
||
184 | 1 | ); |
|
185 | } |
||
186 | |||
187 | // Now let's validate the IPv6 value sent, remove the square brackets ([]) first |
||
188 | 2 | if (!filter_var(trim($this->ip, '[]'), FILTER_VALIDATE_IP, ['flags' => FILTER_FLAG_IPV6,])) { |
|
189 | 1 | throw new Exception("The IPv6 address '{$this->ip}' is invalid."); |
|
190 | } |
||
191 | 1 | } else { |
|
192 | // We have IPv4 with a port defined |
||
193 | 140 | if (strstr($ip_address, ':')) { |
|
194 | 139 | list($this->ip, $this->port_client) = explode(':', $ip_address); |
|
195 | |||
196 | // Type case the port |
||
197 | 139 | $this->port_client = (int)$this->port_client; |
|
198 | 139 | } else { |
|
199 | // No port, fail |
||
200 | 1 | throw new Exception( |
|
201 | 1 | "The host address '{$ip_address}' is missing the port. All " |
|
202 | . "servers must have a port defined!" |
||
203 | 1 | ); |
|
204 | } |
||
205 | |||
206 | // Validate the IPv4 value, if FALSE is not a valid IP, maybe a hostname. Try to resolve |
||
207 | 139 | if (!filter_var($this->ip, FILTER_VALIDATE_IP, ['flags' => FILTER_FLAG_IPV4,]) |
|
208 | 139 | && $this->ip === gethostbyname($this->ip) |
|
209 | 139 | ) { |
|
210 | // When gethostbyname() fails it returns the original string |
||
211 | // so if ip and the result from gethostbyname() are equal this failed. |
||
212 | 1 | throw new Exception("Unable to resolve the host '{$this->ip}' to an IP address."); |
|
213 | } |
||
214 | } |
||
215 | 139 | } |
|
216 | |||
217 | /** |
||
218 | * Check and set any server specific options |
||
219 | */ |
||
220 | 138 | protected function checkAndSetServerOptions() |
|
221 | { |
||
222 | |||
223 | // Specific query port defined |
||
224 | 138 | if (array_key_exists(self::SERVER_OPTIONS_QUERY_PORT, $this->options)) { |
|
225 | 17 | $this->port_query = (int)$this->options[self::SERVER_OPTIONS_QUERY_PORT]; |
|
226 | 17 | } else { |
|
227 | // Do math based on the protocol class |
||
228 | 121 | $this->port_query = $this->protocol->findQueryPort($this->port_client); |
|
229 | } |
||
230 | 138 | } |
|
231 | |||
232 | /** |
||
233 | * Set an option for this server |
||
234 | * |
||
235 | * @param $key |
||
236 | * @param $value |
||
237 | * |
||
238 | * @return $this |
||
239 | */ |
||
240 | 1 | public function setOption($key, $value) |
|
247 | |||
248 | /** |
||
249 | * Return set option value |
||
250 | * |
||
251 | * @param mixed $key |
||
252 | * |
||
253 | * @return mixed |
||
254 | */ |
||
255 | 1 | public function getOption($key) |
|
260 | |||
261 | /** |
||
262 | * Get the ID for this server |
||
263 | * |
||
264 | * @return string |
||
265 | */ |
||
266 | 1 | public function id() |
|
271 | |||
272 | /** |
||
273 | * Get the IP address for this server |
||
274 | * |
||
275 | * @return string |
||
276 | */ |
||
277 | 114 | public function ip() |
|
282 | |||
283 | /** |
||
284 | * Get the client port for this server |
||
285 | * |
||
286 | * @return int |
||
287 | */ |
||
288 | 118 | public function portClient() |
|
289 | { |
||
290 | |||
291 | 118 | return $this->port_client; |
|
292 | } |
||
293 | |||
294 | /** |
||
295 | * Get the query port for this server |
||
296 | * |
||
297 | * @return int |
||
298 | */ |
||
299 | 111 | public function portQuery() |
|
300 | { |
||
301 | |||
302 | 111 | return $this->port_query; |
|
303 | } |
||
304 | |||
305 | /** |
||
306 | * Return the protocol class for this server |
||
307 | * |
||
308 | * @return \GameQ\Protocol |
||
309 | */ |
||
310 | 127 | public function protocol() |
|
315 | |||
316 | /** |
||
317 | * Get the join link for this server |
||
318 | * |
||
319 | * @return string |
||
320 | */ |
||
321 | 111 | public function getJoinLink() |
|
322 | { |
||
323 | |||
324 | 111 | return sprintf($this->protocol->joinLink(), $this->ip, $this->portClient()); |
|
325 | } |
||
326 | |||
327 | /* |
||
328 | * Socket holding |
||
329 | */ |
||
330 | |||
331 | /** |
||
332 | * Add a socket for this server to be reused |
||
333 | * |
||
334 | * @codeCoverageIgnore |
||
335 | * |
||
336 | * @param \GameQ\Query\Core $socket |
||
337 | */ |
||
338 | public function socketAdd(Query\Core $socket) |
||
343 | |||
344 | /** |
||
345 | * Get a socket from the list to reuse, if any are available |
||
346 | * |
||
347 | * @codeCoverageIgnore |
||
348 | * |
||
349 | * @return \GameQ\Query\Core|null |
||
350 | */ |
||
351 | public function socketGet() |
||
362 | |||
363 | /** |
||
364 | * Clear any sockets still listed and attempt to close them |
||
365 | * |
||
366 | * @codeCoverageIgnore |
||
367 | */ |
||
368 | public function socketCleanse() |
||
380 | } |
||
381 |