1: <?php
2: /**
3: * This file is part of GameQ.
4: *
5: * GameQ is free software; you can redistribute it and/or modify
6: * it under the terms of the GNU Lesser General Public License as published by
7: * the Free Software Foundation; either version 3 of the License, or
8: * (at your option) any later version.
9: *
10: * GameQ is distributed in the hope that it will be useful,
11: * but WITHOUT ANY WARRANTY; without even the implied warranty of
12: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13: * GNU Lesser General Public License for more details.
14: *
15: * You should have received a copy of the GNU Lesser General Public License
16: * along with this program. If not, see <http://www.gnu.org/licenses/>.
17: */
18:
19: namespace GameQ;
20:
21: use GameQ\Exception\Server as Exception;
22:
23: /**
24: * Server class to represent each server entity
25: *
26: * @author Austin Bischoff <austin@codebeard.com>
27: */
28: class Server
29: {
30: // Server array keys
31: const SERVER_TYPE = 'type';
32:
33: const SERVER_HOST = 'host';
34:
35: const SERVER_ID = 'id';
36:
37: const SERVER_OPTIONS = 'options';
38:
39: // Server options keys
40:
41: // Use this option when the query_port and client connect ports are different
42: const SERVER_OPTIONS_QUERY_PORT = 'query_port';
43:
44: /**
45: * The protocol class for this server
46: *
47: * @var \GameQ\Protocol
48: */
49: protected $protocol = null;
50:
51: /**
52: * Id of this server
53: *
54: * @var string
55: */
56: public $id = null;
57:
58: /**
59: * IP Address of this server
60: *
61: * @var string
62: */
63: public $ip = null;
64:
65: /**
66: * The server's client port (connect port)
67: *
68: * @var int
69: */
70: public $port_client = null;
71:
72: /**
73: * The server's query port
74: *
75: * @var int
76: */
77: public $port_query = null;
78:
79: /**
80: * Holds other server specific options
81: *
82: * @var array
83: */
84: protected $options = [];
85:
86: /**
87: * Holds the sockets already open for this server
88: *
89: * @var array
90: */
91: protected $sockets = [];
92:
93: /**
94: * Construct the class with the passed options
95: *
96: * @param array $server_info
97: *
98: * @throws \GameQ\Exception\Server
99: */
100: public function __construct(array $server_info = [])
101: {
102: // Check for server type
103: if (!array_key_exists(self::SERVER_TYPE, $server_info) || empty($server_info[self::SERVER_TYPE])) {
104: throw new Exception("Missing server info key '" . self::SERVER_TYPE . "'!");
105: }
106:
107: // Check for server host
108: if (!array_key_exists(self::SERVER_HOST, $server_info) || empty($server_info[self::SERVER_HOST])) {
109: throw new Exception("Missing server info key '" . self::SERVER_HOST . "'!");
110: }
111:
112: // IP address and port check
113: $this->checkAndSetIpPort($server_info[self::SERVER_HOST]);
114:
115: // Check for server id
116: if (array_key_exists(self::SERVER_ID, $server_info) && !empty($server_info[self::SERVER_ID])) {
117: // Set the server id
118: $this->id = $server_info[self::SERVER_ID];
119: } else {
120: // Make an id so each server has an id when returned
121: $this->id = sprintf('%s:%d', $this->ip, $this->port_client);
122: }
123:
124: // Check and set server options
125: if (array_key_exists(self::SERVER_OPTIONS, $server_info)) {
126: // Set the options
127: $this->options = $server_info[self::SERVER_OPTIONS];
128: }
129:
130: try {
131: // Make the protocol class for this type
132: $class = new \ReflectionClass(
133: sprintf('GameQ\\Protocols\\%s', ucfirst(strtolower($server_info[self::SERVER_TYPE])))
134: );
135:
136: $this->protocol = $class->newInstanceArgs([$this->options]);
137: } catch (\ReflectionException $e) {
138: throw new Exception("Unable to locate Protocols class for '{$server_info[self::SERVER_TYPE]}'!");
139: }
140:
141: // Check and set any server options
142: $this->checkAndSetServerOptions();
143:
144: unset($server_info, $class);
145: }
146:
147: /**
148: * Check and set the ip address for this server
149: *
150: * @param $ip_address
151: *
152: * @throws \GameQ\Exception\Server
153: */
154: protected function checkAndSetIpPort($ip_address)
155: {
156: // Test for IPv6
157: if (substr_count($ip_address, ':') > 1) {
158: // See if we have a port, input should be in the format [::1]:27015 or similar
159: if (strstr($ip_address, ']:')) {
160: // Explode to get port
161: $server_addr = explode(':', $ip_address);
162:
163: // Port is the last item in the array, remove it and save
164: $this->port_client = (int)array_pop($server_addr);
165:
166: // The rest is the address, recombine
167: $this->ip = implode(':', $server_addr);
168:
169: unset($server_addr);
170: } else {
171: // Just the IPv6 address, no port defined, fail
172: throw new Exception(
173: "The host address '{$ip_address}' is missing the port. All "
174: . "servers must have a port defined!"
175: );
176: }
177:
178: // Now let's validate the IPv6 value sent, remove the square brackets ([]) first
179: if (!filter_var(trim($this->ip, '[]'), FILTER_VALIDATE_IP, ['flags' => FILTER_FLAG_IPV6,])) {
180: throw new Exception("The IPv6 address '{$this->ip}' is invalid.");
181: }
182: } else {
183: // We have IPv4 with a port defined
184: if (strstr($ip_address, ':')) {
185: list($this->ip, $this->port_client) = explode(':', $ip_address);
186:
187: // Type case the port
188: $this->port_client = (int)$this->port_client;
189: } else {
190: // No port, fail
191: throw new Exception(
192: "The host address '{$ip_address}' is missing the port. All "
193: . "servers must have a port defined!"
194: );
195: }
196:
197: // Validate the IPv4 value, if FALSE is not a valid IP, maybe a hostname.
198: if (! filter_var($this->ip, FILTER_VALIDATE_IP, ['flags' => FILTER_FLAG_IPV4,])) {
199: // Try to resolve the hostname to IPv4
200: $resolved = gethostbyname($this->ip);
201:
202: // When gethostbyname() fails it returns the original string
203: if ($this->ip === $resolved) {
204: // so if ip and the result from gethostbyname() are equal this failed.
205: throw new Exception("Unable to resolve the host '{$this->ip}' to an IP address.");
206: } else {
207: $this->ip = $resolved;
208: }
209: }
210: }
211: }
212:
213: /**
214: * Check and set any server specific options
215: */
216: protected function checkAndSetServerOptions()
217: {
218: // Specific query port defined
219: if (array_key_exists(self::SERVER_OPTIONS_QUERY_PORT, $this->options)) {
220: $this->port_query = (int)$this->options[self::SERVER_OPTIONS_QUERY_PORT];
221: } else {
222: // Do math based on the protocol class
223: $this->port_query = $this->protocol->findQueryPort($this->port_client);
224: }
225: }
226:
227: /**
228: * Set an option for this server
229: *
230: * @param $key
231: * @param $value
232: *
233: * @return $this
234: */
235: public function setOption($key, $value)
236: {
237: $this->options[$key] = $value;
238:
239: return $this; // Make chainable
240: }
241:
242: /**
243: * Return set option value
244: *
245: * @param mixed $key
246: *
247: * @return mixed
248: */
249: public function getOption($key)
250: {
251: return (array_key_exists($key, $this->options)) ? $this->options[$key] : null;
252: }
253:
254: public function getOptions()
255: {
256: return $this->options;
257: }
258:
259: /**
260: * Get the ID for this server
261: *
262: * @return string
263: */
264: public function id()
265: {
266: return $this->id;
267: }
268:
269: /**
270: * Get the IP address for this server
271: *
272: * @return string
273: */
274: public function ip()
275: {
276: return $this->ip;
277: }
278:
279: /**
280: * Get the client port for this server
281: *
282: * @return int
283: */
284: public function portClient()
285: {
286: return $this->port_client;
287: }
288:
289: /**
290: * Get the query port for this server
291: *
292: * @return int
293: */
294: public function portQuery()
295: {
296: return $this->port_query;
297: }
298:
299: /**
300: * Return the protocol class for this server
301: *
302: * @return \GameQ\Protocol
303: */
304: public function protocol()
305: {
306: return $this->protocol;
307: }
308:
309: /**
310: * Get the join link for this server
311: *
312: * @return null|string
313: */
314: public function getJoinLink()
315: {
316: // Read the joinLink template defined by the Protocol
317: $joinLink = $this->protocol->joinLink();
318:
319: // Ensure the Protocol provides a joinLink template
320: if (is_null($joinLink)) {
321: return null;
322: }
323:
324: // Fill the template to build the final joinLink
325: return sprintf($joinLink, $this->ip, $this->portClient());
326: }
327:
328: // Socket holding
329:
330: /**
331: * Add a socket for this server to be reused
332: *
333: * @codeCoverageIgnore
334: *
335: * @param \GameQ\Query\Core $socket
336: */
337: public function socketAdd(Query\Core $socket)
338: {
339: $this->sockets[] = $socket;
340: }
341:
342: /**
343: * Get a socket from the list to reuse, if any are available
344: *
345: * @codeCoverageIgnore
346: *
347: * @return \GameQ\Query\Core|null
348: */
349: public function socketGet()
350: {
351: $socket = null;
352:
353: if (count($this->sockets) > 0) {
354: $socket = array_pop($this->sockets);
355: }
356:
357: return $socket;
358: }
359:
360: /**
361: * Clear any sockets still listed and attempt to close them
362: *
363: * @codeCoverageIgnore
364: */
365: public function socketCleanse()
366: {
367: // Close all of the sockets available
368: foreach ($this->sockets as $socket) {
369: // @var $socket \GameQ\Query\Core
370: $socket->close();
371: }
372:
373: // Reset the sockets list
374: $this->sockets = [];
375: }
376: }
377: