ARC::command()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 1
dl 0
loc 8
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/**
3
 * ARC is a lightweight class, helping you to send commands to your ARMA server via RCon.
4
 *
5
 * @author    Felix Schäfer
6
 * @copyright 2017 Felix Schäfer
7
 * @license   MIT-License
8
 * @link      https://github.com/Nizarii/arma-rcon-php-class Github repository of ARC
9
 * @version   2.2
10
 *
11
 *  For the full copyright and license information, please view the LICENSE
12
 *  file that was distributed with this source code.
13
 */
14
namespace Nizarii;
15
class ARC
16
{
17
    /**
18
     * @var array Options for ARC stored in an array
19
     */
20
    private $options = [
21
        'timeoutSec'    => 1,
22
        'autosaveBans'  => false,
23
        'sendHeartbeat' => false,
24
    ];
25
    /**
26
     * @var string Server IP of the BattlEye server
27
     */
28
    private $serverIP;
29
    /**
30
     * @var int Specific port of the BattlEye server
31
     */
32
    private $serverPort;
33
    /**
34
     * @var string Required password for authenticating
35
     */
36
    private $rconPassword;
37
    /**
38
     * @var resource Socket for sending commands
39
     */
40
    private $socket;
41
    /**
42
     * @var bool Status of the connection
43
     */
44
    private $disconnected = true;
45
    /**
46
     * @var string Head of the message, which was sent to the server
47
     */
48
    private $head;
49
    /**
50
     * Class constructor
51
     *
52
     * @param string $serverIP      IP of the Arma server
53
     * @param integer $serverPort   Port of the Arma server
54
     * @param string $RConPassword  RCon password required by BattlEye
55
     * @param array $options        Options array of ARC
56
     *
57
     * @throws \Exception if wrong parameter types were passed to the function
58
     */
59
    public function __construct($serverIP, $RConPassword, $serverPort = 2302, array $options = array())
60
    {
61
        if (!is_int($serverPort) || !is_string($RConPassword) || !is_string($serverIP)) {
62
            throw new \Exception('Wrong constructor parameter type(s)!');
63
        }
64
        $this->serverIP = $serverIP;
65
        $this->serverPort = $serverPort;
66
        $this->rconPassword = $RConPassword;
67
        $this->options = array_merge($this->options, $options);
68
        $this->checkOptionTypes();
69
        $this->checkForDeprecatedOptions();
70
        $this->connect();
71
    }
72
    /**
73
     * Class destructor
74
     */
75
    public function __destruct()
76
    {
77
        $this->disconnect();
78
    }
79
    /**
80
     * Closes the connection
81
     */
82
    public function disconnect()
83
    {
84
        if ($this->disconnected) {
85
            return;
86
        }
87
        fclose($this->socket);
88
        $this->socket = null;
89
        $this->disconnected = true;
90
    }
91
    /**
92
     * Creates a connection to the server
93
     *
94
     * @throws \Exception if creating the socket fails
95
     */
96
    private function connect()
97
    {
98
        if (!$this->disconnected) {
99
            $this->disconnect();
100
        }
101
        $this->socket = @fsockopen("udp://$this->serverIP", $this->serverPort, $errno, $errstr, $this->options['timeoutSec']);
102
        if (!$this->socket) {
103
            throw new \Exception('Failed to create socket!');
104
        }
105
        stream_set_timeout($this->socket, $this->options['timeoutSec']);
106
        stream_set_blocking($this->socket, true);
107
        $this->authorize();
108
        $this->disconnected = false;
109
    }
110
    /**
111
     * Closes the current connection and creates a new one
112
     */
113
    public function reconnect()
114
    {
115
        if (!$this->disconnected) {
116
            $this->disconnect();
117
        }
118
        $this->connect();
119
        return $this;
120
    }
121
    /**
122
     * Checks if ARC's option array contains any deprecated options
123
     */
124
    private function checkForDeprecatedOptions()
125
    {
126
        if (array_key_exists('timeout_sec', $this->options)) {
127
            @trigger_error("The 'timeout_sec' option is deprecated since version 2.1.2 and will be removed in 3.0. Use 'timeoutSec' instead.", E_USER_DEPRECATED);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
128
            $this->options['timeoutSec'] = $this->options['timeout_sec'];
129
        }
130
        if (array_key_exists('heartbeat', $this->options) || array_key_exists('sendHeartbeat', $this->options)) {
131
            @trigger_error("Sending a heartbeat packet is deprecated since version 2.2.", E_USER_DEPRECATED);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
132
        }
133
    }
134
    /**
135
     * Validate all option types
136
     */
137
    private function checkOptionTypes()
138
    {
139
        if (!is_int($this->options['timeoutSec'])) {
140
            throw new \Exception(
141
                sprintf("Expected option 'timeoutSec' to be integer, got %s", gettype($this->options['timeoutSec']))
142
            );
143
        }
144 View Code Duplication
        if (!is_bool($this->options['sendHeartbeat'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
145
            throw new \Exception(
146
                sprintf("Expected option 'sendHeartbeat' to be boolean, got %s", gettype($this->options['sendHeartbeat']))
147
            );
148
        }
149 View Code Duplication
        if (!is_bool($this->options['autosaveBans'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
150
            throw new \Exception(
151
                sprintf("Expected option 'autosaveBans' to be boolean, got %s", gettype($this->options['autosaveBans']))
152
            );
153
        }
154
    }
155
    /**
156
     * Sends the login data to the server in order to send commands later
157
     *
158
     * @throws \Exception if login fails (due to a wrong password or port)
159
     */
160
    private function authorize()
161
    {
162
        $sent = $this->writeToSocket($this->getLoginMessage());
163
        if ($sent === false) {
164
            throw new \Exception('Failed to send login!');
165
        }
166
        $result = fread($this->socket, 16);
167
        if (@ord($result[strlen($result)-1]) == 0) {
168
            throw new \Exception('Login failed, wrong password or wrong port!');
169
        }
170
    }
171
    /**
172
     * Receives the answer form the server
173
     *
174
     * @return string Any answer from the server, except the log-in message
175
     */
176
    protected function getResponse()
177
    {
178
        $get = function() {
179
            return substr(fread($this->socket, 102400), strlen($this->head));
180
        };
181
182
        $output = '';
183
        do {
184
            $answer = $get();
185
            while (strpos($answer, 'RCon admin') !== false) {
186
                $answer = $get();
187
            }
188
            $output .= $answer;
189
        } while (!empty($answer));
190
        return $output;
191
    }
192
    /**
193
     * The heart of this class - this function actually sends the RCon command
194
     *
195
     * @param string $command The command sent to the server
196
     *
197
     * @throws \Exception if the connection is closed
198
     * @throws \Exception if sending the command failed
199
     *
200
     * @return bool Whether sending the command was successful or not
201
     */
202
    protected function send($command)
203
    {
204
        if ($this->disconnected) {
205
            throw new \Exception('Failed to send command, because the connection is closed!');
206
        }
207
        $msgCRC = $this->getMsgCRC($command);
208
        $head = 'BE'.chr(hexdec($msgCRC[0])).chr(hexdec($msgCRC[1])).chr(hexdec($msgCRC[2])).chr(hexdec($msgCRC[3])).chr(hexdec('ff')).chr(hexdec('01')).chr(hexdec(sprintf('%01b', 0)));
209
        $msg = $head.$command;
210
        $this->head = $head;
211
        if ($this->writeToSocket($msg) === false) {
212
            throw new \Exception('Failed to send command!');
213
        }
214
    }
215
    /**
216
     * Writes the given message to the socket
217
     *
218
     * @param string $message Message which will be written to the socket
219
     *
220
     * @return int
221
     */
222
    private function writeToSocket($message)
223
    {
224
        return fwrite($this->socket, $message);
225
    }
226
    /**
227
     * Generates the password's CRC32 data
228
     *
229
     * @return string
230
     */
231
    private function getAuthCRC()
232
    {
233
        $authCRC = sprintf('%x', crc32(chr(255).chr(00).trim($this->rconPassword)));
234
        $authCRC = array(substr($authCRC,-2,2), substr($authCRC,-4,2), substr($authCRC,-6,2), substr($authCRC,0,2));
235
        return $authCRC;
236
    }
237
    /**
238
     * Generates the message's CRC32 data
239
     *
240
     * @param string $command The message which will be prepared for being sent to the server
241
     *
242
     * @return string Message which can be sent to the server
243
     */
244
    private function getMsgCRC($command)
245
    {
246
        $msgCRC = sprintf('%x', crc32(chr(255).chr(01).chr(hexdec(sprintf('%01b', 0))).$command));
247
        $msgCRC = array(substr($msgCRC,-2,2),substr($msgCRC,-4,2),substr($msgCRC,-6,2),substr($msgCRC,0,2));
248
        return $msgCRC;
249
    }
250
    /**
251
     * Generates the login message
252
     *
253
     * @return string The message for authenticating in, containing the RCon password
254
     */
255
    private function getLoginMessage()
256
    {
257
        $authCRC = $this->getAuthCRC();
258
        $loginMsg = 'BE'.chr(hexdec($authCRC[0])).chr(hexdec($authCRC[1])).chr(hexdec($authCRC[2])).chr(hexdec($authCRC[3]));
259
        $loginMsg .= chr(hexdec('ff')).chr(hexdec('00')).$this->rconPassword;
260
        return $loginMsg;
261
    }
262
    /**
263
     * Returns the socket used by ARC, might be null if connection is closed
264
     *
265
     * @return resource
266
     */
267
    public function getSocket()
268
    {
269
        return $this->socket;
270
    }
271
    /**
272
     * Sends a custom command to the server
273
     *
274
     * @param string $command Command which will be sent to the server
275
     *
276
     * @throws \Exception if wrong parameter types were passed to the function
277
     *
278
     * @return string Response from the server
279
     */
280
    public function command($command)
281
    {
282
        if (!is_string($command)) {
283
            throw new \Exception('Wrong parameter type!');
284
        }
285
        $this->send($command);
286
        return $this->getResponse();
287
    }
288
    /**
289
     * Executes multiple commands
290
     *
291
     * @param array $commands Commands to be executed
292
     */
293
    public function commands(array $commands)
294
    {
295
        foreach ($commands as $command) {
296
            if (!is_string($command)) {
297
                continue;
298
            }
299
            $this->command($command);
300
        }
301
    }
302
    /**
303
     * Kicks a player who is currently on the server
304
     *
305
     * @param string $reason  Message displayed why the player is kicked
306
     * @param integer $player The player who should be kicked
307
     * @throws \Exception if wrong parameter types were passed to the function
308
     *
309
     * @return ARC
310
     */
311
    public function kickPlayer($player, $reason = 'Admin Kick')
312
    {
313 View Code Duplication
        if (!is_int($player) && !is_string($player)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
314
            throw new \Exception(
315
                sprintf('Expected parameter 1 to be string or integer, got %s', gettype($player))
316
            );
317
        }
318
        if (!is_string($reason)) {
319
            throw new \Exception(
320
                sprintf('Expected parameter 2 to be string, got %s', gettype($reason))
321
            );
322
        }
323
        $this->send("kick $player $reason");
324
        $this->reconnect();
325
        return $this;
326
    }
327
    /**
328
     * Sends a global message to all players
329
     *
330
     * @param string $message The message which will be shown to all players
331
     * @throws \Exception if wrong parameter types were passed to the function
332
     *
333
     * @return ARC
334
     */
335
    public function sayGlobal($message)
336
    {
337
        if (!is_string($message)) {
338
            throw new \Exception(
339
                sprintf('Expected parameter 1 to be string, got %s', gettype($message))
340
            );
341
        }
342
        $this->send("Say -1 $message");
343
        return $this;
344
    }
345
    /**
346
     * Sends a message to a specific player
347
     *
348
     * @param integer $player Player who will be sent the message to
349
     * @param string $message Message for the player
350
     * @throws \Exception if wrong parameter types were passed to the function
351
     *
352
     * @return ARC
353
     */
354
    public function sayPlayer($player, $message)
355
    {
356
        if (!is_int($player) || !is_string($message)) {
357
            throw new \Exception('Wrong parameter type(s)!');
358
        }
359
        $this->send("Say $player $message");
360
        return $this;
361
    }
362
    /**
363
     * Loads the "scripts.txt" file without the need to restart the server
364
     *
365
     * @return ARC
366
     */
367
    public function loadScripts()
368
    {
369
        $this->send('loadScripts');
370
        return $this;
371
    }
372
    /**
373
     * Changes the MaxPing value. If a player has a higher ping, he will be kicked from the server
374
     *
375
     * @param integer $ping The value for the 'MaxPing' BattlEye server setting
376
     *
377
     * @throws \Exception if wrong parameter types were passed to the function
378
     *
379
     * @return ARC
380
     */
381
    public function maxPing($ping)
382
    {
383
        if (!is_int($ping)) {
384
            throw new \Exception(
385
                sprintf('Expected parameter 1 to be integer, got %s', gettype($ping))
386
            );
387
        }
388
        $this->send("MaxPing $ping");
389
        return $this;
390
    }
391
    /**
392
     * Changes the RCon password
393
     *
394
     * @param string $password The new password
395
     *
396
     * @throws \Exception if wrong parameter types were passed to the function
397
     *
398
     * @return ARC
399
     */
400
    public function changePassword($password)
401
    {
402
        if (!is_string($password)) {
403
            throw new \Exception(
404
                sprintf('Expected parameter 1 to be string, got %s', gettype($password))
405
            );
406
        }
407
        $this->send("RConPassword $password");
408
        return $this;
409
    }
410
    /**
411
     * (Re)load the BE ban list from bans.txt
412
     *
413
     * @return ARC
414
     */
415
    public function loadBans()
416
    {
417
        $this->send('loadBans');
418
        return $this;
419
    }
420
    /**
421
     * Gets a list of all players currently on the server
422
     *
423
     * @return string The list of all players on the server
424
     */
425
    public function getPlayers()
426
    {
427
        $this->send('players');
428
        $result = $this->getResponse();
429
430
        $this->reconnect();
431
        return $result;
432
    }
433
    /**
434
     * Gets a list of all players currently on the server as an array
435
     *
436
     * @author nerdalertdk (https://github.com/nerdalertdk)
437
     * @link https://github.com/Nizarii/arma-rcon-class-php/issues/4 The related GitHub Issue
438
     *
439
     * @throws \Exception if sending the command failed
440
     *
441
     * @return array The array containing all players being currently on the server
442
     */
443
    public function getPlayersArray()
444
    {
445
        $playersRaw = $this->getPlayers();
446
        $players = $this->cleanList($playersRaw);
447
        preg_match_all("#(\d+)\s+(\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d+\b)\s+(\d+)\s+([0-9a-fA-F]+)\(\w+\)\s([\S ]+)$#im", $players, $str);
448
449
        return $this->formatList($str);
450
    }
451
    /**
452
     * Gets a list of all bans
453
     *
454
     * @throws \Exception if sending the command failed
455
     *
456
     * @return string List containing the missions
457
     */
458
    public function getMissions()
459
    {
460
        $this->send('missions');
461
        return $this->getResponse();
462
    }
463
    /**
464
     * Ban a player's BE GUID from the server. If time is not specified or 0, the ban will be permanent;.
465
     * If reason is not specified the player will be kicked with the message "Banned".
466
     *
467
     * @param integer $player Player who will be banned
468
     * @param string $reason  Reason why the player is banned
469
     * @param integer $time   How long the player is banned in minutes (0 = permanent)
470
     * @throws \Exception if wrong parameter types were passed to the function
471
     *
472
     * @return ARC
473
     */
474
    public function banPlayer($player, $reason = 'Banned', $time = 0)
475
    {
476 View Code Duplication
        if (!is_string($player) && !is_int($player)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
477
            throw new \Exception(
478
                sprintf('Expected parameter 1 to be integer or string, got %s', gettype($player))
479
            );
480
        }
481
        if (!is_string($reason) || !is_int($time)) {
482
            throw new \Exception('Wrong parameter type(s)!');
483
        }
484
        $this->send("ban $player $time $reason");
485
        $this->reconnect();
486
487
        if ($this->options['autosavebans']) {
488
            $this->writeBans();
489
        }
490
        return $this;
491
    }
492
    /**
493
     * Same as "banPlayer", but allows to ban a player that is not currently on the server
494
     *
495
     * @param integer $player Player who will be banned
496
     * @param string $reason  Reason why the player is banned
497
     * @param integer $time   How long the player is banned in minutes (0 = permanent)
498
     *
499
     * @throws \Exception if wrong parameter types were passed to the function
500
     *
501
     * @return ARC
502
     */
503
    public function addBan($player, $reason = 'Banned', $time = 0)
504
    {
505 View Code Duplication
        if (!is_string($player) || !is_string($reason) || !is_int($time)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
506
            throw new \Exception('Wrong parameter type(s)!');
507
        }
508
        $this->send("addBan $player $time $reason");
509
510
        if ($this->options['autosavebans']) {
511
            $this->writeBans();
512
        }
513
514
        return $this;
515
    }
516
    /**
517
     * Removes a ban
518
     *
519
     * @param integer $banId Ban who will be removed
520
     *
521
     * @throws \Exception if wrong parameter types were passed to the function
522
     *
523
     * @return ARC
524
     */
525
    public function removeBan($banId)
526
    {
527
        if (!is_int($banId)) {
528
            throw new \Exception(
529
                sprintf('Expected parameter 1 to be integer, got %s', gettype($banId))
530
            );
531
        }
532
        $this->send("removeBan $banId");
533
534
        if ($this->options['autosavebans']) {
535
            $this->writeBans();
536
        }
537
538
        return $this;
539
    }
540
    /**
541
     * Gets an array of all bans
542
     *
543
     * @author nerdalertdk (https://github.com/nerdalertdk)
544
     * @link https://github.com/Nizarii/arma-rcon-class-php/issues/4
545
     *
546
     * @return array The array containing all bans
547
     */
548
    public function getBansArray()
549
    {
550
        $bansRaw = $this->getBans();
551
        $bans = $this->cleanList($bansRaw);
552
        preg_match_all("#(\d+)\s+([0-9a-fA-F]+)\s([perm|\d]+)\s([\S ]+)$#im", $bans, $str);
553
        return $this->formatList($str);
554
    }
555
    /**
556
     * Gets a list of all bans
557
     *
558
     * @return string The response from the server
559
     */
560
    public function getBans()
561
    {
562
        $this->send('bans');
563
        return $this->getResponse();
564
    }
565
    /**
566
     * Removes expired bans from bans file
567
     *
568
     * @return ARC
569
     */
570
    public function writeBans()
571
    {
572
        $this->send('writeBans');
573
        return $this;
574
    }
575
    /**
576
     * Gets the current version of the BE server
577
     *
578
     * @return string The BE server version
579
     */
580
    public function getBEServerVersion()
581
    {
582
        $this->send('version');
583
        return $this->getResponse();
584
    }
585
    /**
586
     * Converts BE text "array" list to array
587
     *
588
     * @author nerdalertdk (https://github.com/nerdalertdk)
589
     * @link https://github.com/Nizarii/arma-rcon-class-php/issues/4 The related Github issue
590
     *
591
     * @param $str array
592
     *
593
     * @return array
594
     */
595
    private function formatList($str)
596
    {
597
        // Remove first array
598
        array_shift($str);
599
        // Create return array
600
        $result = array();
601
        // Loop true the main arrays, each holding a value
602
        foreach($str as $key => $value) {
603
            // Combines each main value into new array
604
            foreach($value as $keyLine => $line) {
605
                $result[$keyLine][$key] = trim($line);
606
            }
607
        }
608
        return $result;
609
    }
610
    /**
611
     * Remove control characters
612
     *
613
     * @author nerdalertdk (https://github.com/nerdalertdk)
614
     * @link https://github.com/Nizarii/arma-rcon-class-php/issues/4 The related GitHub issue
615
     *
616
     * @param $str string
617
     *
618
     * @return string
619
     */
620
    private function cleanList($str)
621
    {
622
        return preg_replace('/[\x00-\x09\x0B\x0C\x0E-\x1F\x7F]/', '', $str);
623
    }
624
}