Passed
Pull Request — v3 (#613)
by
unknown
36:35 queued 01:32
created

Server   A

Complexity

Total Complexity 33

Size/Duplication

Total Lines 361
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 11
Bugs 4 Features 0
Metric Value
wmc 33
eloc 80
c 11
b 4
f 0
dl 0
loc 361
ccs 65
cts 65
cp 1
rs 9.76

15 Methods

Rating   Name   Duplication   Size   Complexity  
B __construct() 0 46 9
A checkAndSetServerOptions() 0 9 2
A setOption() 0 6 1
A getOption() 0 4 2
B checkAndSetIpPort() 0 55 7
A getJoinLink() 0 4 1
A getOptions() 0 3 1
A id() 0 4 1
A socketCleanse() 0 11 2
A ip() 0 4 1
A portQuery() 0 4 1
A portClient() 0 4 1
A socketGet() 0 10 2
A protocol() 0 4 1
A socketAdd() 0 4 1
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 <[email protected]>
27
 */
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 289
    public function __construct(array $server_info = [])
108
    {
109
110
        // Check for server type
111 289
        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 288
        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 287
        $this->checkAndSetIpPort($server_info[self::SERVER_HOST]);
122
123
        // Check for server id
124 283
        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
        } else {
128
            // Make an id so each server has an id when returned
129 282
            $this->id = sprintf('%s:%d', $this->ip, $this->port_client);
130
        }
131
132
        // Check and set server options
133 283
        if (array_key_exists(self::SERVER_OPTIONS, $server_info)) {
134
            // Set the options
135 265
            $this->options = $server_info[self::SERVER_OPTIONS];
136
        }
137
138
        try {
139
            // Make the protocol class for this type
140 283
            $class = new \ReflectionClass(
141 283
                sprintf('GameQ\\Protocols\\%s', ucfirst(strtolower($server_info[self::SERVER_TYPE])))
142
            );
143
144 282
            $this->protocol = $class->newInstanceArgs([$this->options]);
145 1
        } 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 282
        $this->checkAndSetServerOptions();
151
152 282
        unset($server_info, $class);
153 282
    }
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 287
    protected function checkAndSetIpPort($ip_address)
163
    {
164
165
        // Test for IPv6
166 287
        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
            } 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 1
                    . "servers must have a port defined!"
184
                );
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 2
                throw new Exception("The IPv6 address '{$this->ip}' is invalid.");
190
            }
191
        } else {
192
            // We have IPv4 with a port defined
193 284
            if (strstr($ip_address, ':')) {
194 283
                list($this->ip, $this->port_client) = explode(':', $ip_address);
195
196
                // Type case the port
197 283
                $this->port_client = (int)$this->port_client;
198
            } else {
199
                // No port, fail
200 1
                throw new Exception(
201 1
                    "The host address '{$ip_address}' is missing the port. All "
202 1
                    . "servers must have a port defined!"
203
                );
204
            }
205
206
            // Validate the IPv4 value, if FALSE is not a valid IP, maybe a hostname.
207 283
            if (! filter_var($this->ip, FILTER_VALIDATE_IP, ['flags' => FILTER_FLAG_IPV4,])) {
208
                // Try to resolve the hostname to IPv4
209 6
                $resolved = gethostbyname($this->ip);
210
211
                // When gethostbyname() fails it returns the original string
212 6
                if ($this->ip === $resolved) {
213
                    // so if ip and the result from gethostbyname() are equal this failed.
214 1
                    throw new Exception("Unable to resolve the host '{$this->ip}' to an IP address.");
215
                } else {
216 5
                    $this->ip = $resolved;
217
                }
218
            }
219
        }
220 283
    }
221
222
    /**
223
     * Check and set any server specific options
224
     */
225 282
    protected function checkAndSetServerOptions()
226
    {
227
228
        // Specific query port defined
229 282
        if (array_key_exists(self::SERVER_OPTIONS_QUERY_PORT, $this->options)) {
230 19
            $this->port_query = (int)$this->options[self::SERVER_OPTIONS_QUERY_PORT];
231
        } else {
232
            // Do math based on the protocol class
233 263
            $this->port_query = $this->protocol->findQueryPort($this->port_client);
234
        }
235 282
    }
236
237
    /**
238
     * Set an option for this server
239
     *
240
     * @param $key
241
     * @param $value
242
     *
243
     * @return $this
244
     */
245 1
    public function setOption($key, $value)
246
    {
247
248 1
        $this->options[$key] = $value;
249
250 1
        return $this; // Make chainable
251
    }
252
253
    /**
254
     * Return set option value
255
     *
256
     * @param mixed $key
257
     *
258
     * @return mixed
259
     */
260 1
    public function getOption($key)
261
    {
262
263 1
        return (array_key_exists($key, $this->options)) ? $this->options[$key] : null;
264
    }
265
266
    public function getOptions()
267
    {
268
        return $this->options;
269
    }
270
271 2
    /**
272
     * Get the ID for this server
273
     *
274 2
     * @return string
275
     */
276
    public function id()
277
    {
278
279
        return $this->id;
280
    }
281
282 237
    /**
283
     * Get the IP address for this server
284
     *
285 237
     * @return string
286
     */
287
    public function ip()
288
    {
289
290
        return $this->ip;
291
    }
292
293 245
    /**
294
     * Get the client port for this server
295
     *
296 245
     * @return int
297
     */
298
    public function portClient()
299
    {
300
301
        return $this->port_client;
302
    }
303
304 234
    /**
305
     * Get the query port for this server
306
     *
307 234
     * @return int
308
     */
309
    public function portQuery()
310
    {
311
312
        return $this->port_query;
313
    }
314
315 266
    /**
316
     * Return the protocol class for this server
317
     *
318 266
     * @return \GameQ\Protocol
319
     */
320
    public function protocol()
321
    {
322
323
        return $this->protocol;
324
    }
325
326 238
    /**
327
     * Get the join link for this server
328
     *
329 238
     * @return string
330
     */
331
    public function getJoinLink()
332
    {
333
334
        return sprintf($this->protocol->joinLink(), $this->ip, $this->portClient());
335
    }
336
337
    /*
338
     * Socket holding
339
     */
340
341
    /**
342
     * Add a socket for this server to be reused
343
     *
344
     * @codeCoverageIgnore
345
     *
346
     * @param \GameQ\Query\Core $socket
347
     */
348
    public function socketAdd(Query\Core $socket)
349
    {
350
351
        $this->sockets[] = $socket;
352
    }
353
354
    /**
355
     * Get a socket from the list to reuse, if any are available
356
     *
357
     * @codeCoverageIgnore
358
     *
359
     * @return \GameQ\Query\Core|null
360
     */
361
    public function socketGet()
362
    {
363
364
        $socket = null;
365
366
        if (count($this->sockets) > 0) {
367
            $socket = array_pop($this->sockets);
368
        }
369
370
        return $socket;
371
    }
372
373
    /**
374
     * Clear any sockets still listed and attempt to close them
375
     *
376
     * @codeCoverageIgnore
377
     */
378
    public function socketCleanse()
379
    {
380
381
        // Close all of the sockets available
382
        foreach ($this->sockets as $socket) {
383
            /* @var $socket \GameQ\Query\Core */
384
            $socket->close();
385
        }
386
387
        // Reset the sockets list
388
        $this->sockets = [];
389
    }
390
}
391