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\Protocols;
20:
21: use GameQ\Exception\Protocol as Exception;
22: use GameQ\Helpers\Str;
23: use GameQ\Protocol;
24: use GameQ\Result;
25:
26: /**
27: * Ventrilo Protocol Class
28: *
29: * Note that a password is not required for versions >= 3.0.3
30: *
31: * All values are utf8 encoded upon processing
32: *
33: * This code ported from GameQ v1/v2. Credit to original author(s) as I just updated it to
34: * work within this new system.
35: *
36: * @author Austin Bischoff <austin@codebeard.com>
37: */
38: class Ventrilo extends Protocol
39: {
40: /**
41: * Array of packets we want to look up.
42: * Each key should correspond to a defined method in this or a parent class
43: *
44: * @var array
45: */
46: protected $packets = [
47: self::PACKET_ALL =>
48: "V\xc8\xf4\xf9`\xa2\x1e\xa5M\xfb\x03\xccQN\xa1\x10\x95\xaf\xb2g\x17g\x812\xfbW\xfd\x8e\xd2\x22r\x034z\xbb\x98",
49: ];
50:
51: /**
52: * The query protocol used to make the call
53: *
54: * @var string
55: */
56: protected $protocol = 'ventrilo';
57:
58: /**
59: * String name of this protocol class
60: *
61: * @var string
62: */
63: protected $name = 'ventrilo';
64:
65: /**
66: * Longer string name of this protocol class
67: *
68: * @var string
69: */
70: protected $name_long = "Ventrilo";
71:
72: /**
73: * The client join link
74: *
75: * @var string
76: */
77: protected $join_link = "ventrilo://%s:%d/";
78:
79: /**
80: * Normalize settings for this protocol
81: *
82: * @var array
83: */
84: protected $normalize = [
85: // General
86: 'general' => [
87: 'dedicated' => 'dedicated',
88: 'password' => 'auth',
89: 'hostname' => 'name',
90: 'numplayers' => 'clientcount',
91: 'maxplayers' => 'maxclients',
92: ],
93: // Player
94: 'player' => [
95: 'team' => 'cid',
96: 'name' => 'name',
97: ],
98: // Team
99: 'team' => [
100: 'id' => 'cid',
101: 'name' => 'name',
102: ],
103: ];
104:
105: /**
106: * Encryption table for the header
107: *
108: * @var array
109: */
110: private $head_encrypt_table = [
111: 0x80,
112: 0xe5,
113: 0x0e,
114: 0x38,
115: 0xba,
116: 0x63,
117: 0x4c,
118: 0x99,
119: 0x88,
120: 0x63,
121: 0x4c,
122: 0xd6,
123: 0x54,
124: 0xb8,
125: 0x65,
126: 0x7e,
127: 0xbf,
128: 0x8a,
129: 0xf0,
130: 0x17,
131: 0x8a,
132: 0xaa,
133: 0x4d,
134: 0x0f,
135: 0xb7,
136: 0x23,
137: 0x27,
138: 0xf6,
139: 0xeb,
140: 0x12,
141: 0xf8,
142: 0xea,
143: 0x17,
144: 0xb7,
145: 0xcf,
146: 0x52,
147: 0x57,
148: 0xcb,
149: 0x51,
150: 0xcf,
151: 0x1b,
152: 0x14,
153: 0xfd,
154: 0x6f,
155: 0x84,
156: 0x38,
157: 0xb5,
158: 0x24,
159: 0x11,
160: 0xcf,
161: 0x7a,
162: 0x75,
163: 0x7a,
164: 0xbb,
165: 0x78,
166: 0x74,
167: 0xdc,
168: 0xbc,
169: 0x42,
170: 0xf0,
171: 0x17,
172: 0x3f,
173: 0x5e,
174: 0xeb,
175: 0x74,
176: 0x77,
177: 0x04,
178: 0x4e,
179: 0x8c,
180: 0xaf,
181: 0x23,
182: 0xdc,
183: 0x65,
184: 0xdf,
185: 0xa5,
186: 0x65,
187: 0xdd,
188: 0x7d,
189: 0xf4,
190: 0x3c,
191: 0x4c,
192: 0x95,
193: 0xbd,
194: 0xeb,
195: 0x65,
196: 0x1c,
197: 0xf4,
198: 0x24,
199: 0x5d,
200: 0x82,
201: 0x18,
202: 0xfb,
203: 0x50,
204: 0x86,
205: 0xb8,
206: 0x53,
207: 0xe0,
208: 0x4e,
209: 0x36,
210: 0x96,
211: 0x1f,
212: 0xb7,
213: 0xcb,
214: 0xaa,
215: 0xaf,
216: 0xea,
217: 0xcb,
218: 0x20,
219: 0x27,
220: 0x30,
221: 0x2a,
222: 0xae,
223: 0xb9,
224: 0x07,
225: 0x40,
226: 0xdf,
227: 0x12,
228: 0x75,
229: 0xc9,
230: 0x09,
231: 0x82,
232: 0x9c,
233: 0x30,
234: 0x80,
235: 0x5d,
236: 0x8f,
237: 0x0d,
238: 0x09,
239: 0xa1,
240: 0x64,
241: 0xec,
242: 0x91,
243: 0xd8,
244: 0x8a,
245: 0x50,
246: 0x1f,
247: 0x40,
248: 0x5d,
249: 0xf7,
250: 0x08,
251: 0x2a,
252: 0xf8,
253: 0x60,
254: 0x62,
255: 0xa0,
256: 0x4a,
257: 0x8b,
258: 0xba,
259: 0x4a,
260: 0x6d,
261: 0x00,
262: 0x0a,
263: 0x93,
264: 0x32,
265: 0x12,
266: 0xe5,
267: 0x07,
268: 0x01,
269: 0x65,
270: 0xf5,
271: 0xff,
272: 0xe0,
273: 0xae,
274: 0xa7,
275: 0x81,
276: 0xd1,
277: 0xba,
278: 0x25,
279: 0x62,
280: 0x61,
281: 0xb2,
282: 0x85,
283: 0xad,
284: 0x7e,
285: 0x9d,
286: 0x3f,
287: 0x49,
288: 0x89,
289: 0x26,
290: 0xe5,
291: 0xd5,
292: 0xac,
293: 0x9f,
294: 0x0e,
295: 0xd7,
296: 0x6e,
297: 0x47,
298: 0x94,
299: 0x16,
300: 0x84,
301: 0xc8,
302: 0xff,
303: 0x44,
304: 0xea,
305: 0x04,
306: 0x40,
307: 0xe0,
308: 0x33,
309: 0x11,
310: 0xa3,
311: 0x5b,
312: 0x1e,
313: 0x82,
314: 0xff,
315: 0x7a,
316: 0x69,
317: 0xe9,
318: 0x2f,
319: 0xfb,
320: 0xea,
321: 0x9a,
322: 0xc6,
323: 0x7b,
324: 0xdb,
325: 0xb1,
326: 0xff,
327: 0x97,
328: 0x76,
329: 0x56,
330: 0xf3,
331: 0x52,
332: 0xc2,
333: 0x3f,
334: 0x0f,
335: 0xb6,
336: 0xac,
337: 0x77,
338: 0xc4,
339: 0xbf,
340: 0x59,
341: 0x5e,
342: 0x80,
343: 0x74,
344: 0xbb,
345: 0xf2,
346: 0xde,
347: 0x57,
348: 0x62,
349: 0x4c,
350: 0x1a,
351: 0xff,
352: 0x95,
353: 0x6d,
354: 0xc7,
355: 0x04,
356: 0xa2,
357: 0x3b,
358: 0xc4,
359: 0x1b,
360: 0x72,
361: 0xc7,
362: 0x6c,
363: 0x82,
364: 0x60,
365: 0xd1,
366: 0x0d,
367: ];
368:
369: /**
370: * Encryption table for the data
371: *
372: * @var array
373: */
374: private $data_encrypt_table = [
375: 0x82,
376: 0x8b,
377: 0x7f,
378: 0x68,
379: 0x90,
380: 0xe0,
381: 0x44,
382: 0x09,
383: 0x19,
384: 0x3b,
385: 0x8e,
386: 0x5f,
387: 0xc2,
388: 0x82,
389: 0x38,
390: 0x23,
391: 0x6d,
392: 0xdb,
393: 0x62,
394: 0x49,
395: 0x52,
396: 0x6e,
397: 0x21,
398: 0xdf,
399: 0x51,
400: 0x6c,
401: 0x76,
402: 0x37,
403: 0x86,
404: 0x50,
405: 0x7d,
406: 0x48,
407: 0x1f,
408: 0x65,
409: 0xe7,
410: 0x52,
411: 0x6a,
412: 0x88,
413: 0xaa,
414: 0xc1,
415: 0x32,
416: 0x2f,
417: 0xf7,
418: 0x54,
419: 0x4c,
420: 0xaa,
421: 0x6d,
422: 0x7e,
423: 0x6d,
424: 0xa9,
425: 0x8c,
426: 0x0d,
427: 0x3f,
428: 0xff,
429: 0x6c,
430: 0x09,
431: 0xb3,
432: 0xa5,
433: 0xaf,
434: 0xdf,
435: 0x98,
436: 0x02,
437: 0xb4,
438: 0xbe,
439: 0x6d,
440: 0x69,
441: 0x0d,
442: 0x42,
443: 0x73,
444: 0xe4,
445: 0x34,
446: 0x50,
447: 0x07,
448: 0x30,
449: 0x79,
450: 0x41,
451: 0x2f,
452: 0x08,
453: 0x3f,
454: 0x42,
455: 0x73,
456: 0xa7,
457: 0x68,
458: 0xfa,
459: 0xee,
460: 0x88,
461: 0x0e,
462: 0x6e,
463: 0xa4,
464: 0x70,
465: 0x74,
466: 0x22,
467: 0x16,
468: 0xae,
469: 0x3c,
470: 0x81,
471: 0x14,
472: 0xa1,
473: 0xda,
474: 0x7f,
475: 0xd3,
476: 0x7c,
477: 0x48,
478: 0x7d,
479: 0x3f,
480: 0x46,
481: 0xfb,
482: 0x6d,
483: 0x92,
484: 0x25,
485: 0x17,
486: 0x36,
487: 0x26,
488: 0xdb,
489: 0xdf,
490: 0x5a,
491: 0x87,
492: 0x91,
493: 0x6f,
494: 0xd6,
495: 0xcd,
496: 0xd4,
497: 0xad,
498: 0x4a,
499: 0x29,
500: 0xdd,
501: 0x7d,
502: 0x59,
503: 0xbd,
504: 0x15,
505: 0x34,
506: 0x53,
507: 0xb1,
508: 0xd8,
509: 0x50,
510: 0x11,
511: 0x83,
512: 0x79,
513: 0x66,
514: 0x21,
515: 0x9e,
516: 0x87,
517: 0x5b,
518: 0x24,
519: 0x2f,
520: 0x4f,
521: 0xd7,
522: 0x73,
523: 0x34,
524: 0xa2,
525: 0xf7,
526: 0x09,
527: 0xd5,
528: 0xd9,
529: 0x42,
530: 0x9d,
531: 0xf8,
532: 0x15,
533: 0xdf,
534: 0x0e,
535: 0x10,
536: 0xcc,
537: 0x05,
538: 0x04,
539: 0x35,
540: 0x81,
541: 0xb2,
542: 0xd5,
543: 0x7a,
544: 0xd2,
545: 0xa0,
546: 0xa5,
547: 0x7b,
548: 0xb8,
549: 0x75,
550: 0xd2,
551: 0x35,
552: 0x0b,
553: 0x39,
554: 0x8f,
555: 0x1b,
556: 0x44,
557: 0x0e,
558: 0xce,
559: 0x66,
560: 0x87,
561: 0x1b,
562: 0x64,
563: 0xac,
564: 0xe1,
565: 0xca,
566: 0x67,
567: 0xb4,
568: 0xce,
569: 0x33,
570: 0xdb,
571: 0x89,
572: 0xfe,
573: 0xd8,
574: 0x8e,
575: 0xcd,
576: 0x58,
577: 0x92,
578: 0x41,
579: 0x50,
580: 0x40,
581: 0xcb,
582: 0x08,
583: 0xe1,
584: 0x15,
585: 0xee,
586: 0xf4,
587: 0x64,
588: 0xfe,
589: 0x1c,
590: 0xee,
591: 0x25,
592: 0xe7,
593: 0x21,
594: 0xe6,
595: 0x6c,
596: 0xc6,
597: 0xa6,
598: 0x2e,
599: 0x52,
600: 0x23,
601: 0xa7,
602: 0x20,
603: 0xd2,
604: 0xd7,
605: 0x28,
606: 0x07,
607: 0x23,
608: 0x14,
609: 0x24,
610: 0x3d,
611: 0x45,
612: 0xa5,
613: 0xc7,
614: 0x90,
615: 0xdb,
616: 0x77,
617: 0xdd,
618: 0xea,
619: 0x38,
620: 0x59,
621: 0x89,
622: 0x32,
623: 0xbc,
624: 0x00,
625: 0x3a,
626: 0x6d,
627: 0x61,
628: 0x4e,
629: 0xdb,
630: 0x29,
631: ];
632:
633: /**
634: * Process the response
635: *
636: * @return array
637: * @throws \GameQ\Exception\Protocol
638: */
639: public function processResponse()
640: {
641: // We need to decrypt the packets
642: $decrypted = $this->decryptPackets($this->packets_response);
643:
644: // Now let us convert special characters from hex to ascii all at once
645: $decrypted = preg_replace_callback(
646: '|%([0-9A-F]{2})|',
647: function ($matches) {
648: // Pack this into ascii
649: return pack('H*', $matches[1]);
650: },
651: $decrypted
652: );
653:
654: // Explode into lines
655: $lines = explode("\n", $decrypted);
656:
657: // Set the result to a new result instance
658: $result = new Result();
659:
660: // Always dedicated
661: $result->add('dedicated', 1);
662:
663: // Defaults
664: $channelFields = 5;
665: $playerFields = 7;
666:
667: // Iterate over the lines
668: foreach ($lines as $line) {
669: // Trim all the outlying space
670: $line = trim($line);
671:
672: // We dont have anything in this line
673: if (strlen($line) == 0) {
674: continue;
675: }
676:
677: /**
678: * Everything is in this format: ITEM: VALUE
679: *
680: * Example:
681: * ...
682: * MAXCLIENTS: 175
683: * VOICECODEC: 3,Speex
684: * VOICEFORMAT: 31,32 KHz%2C 16 bit%2C 9 Qlty
685: * UPTIME: 9167971
686: * PLATFORM: Linux-i386
687: * VERSION: 3.0.6
688: * ...
689: */
690:
691: // Check to see if we have a colon, every line should
692: if (($colon_pos = strpos($line, ":")) !== false && $colon_pos > 0) {
693: // Split the line into key/value pairs
694: list($key, $value) = explode(':', $line, 2);
695:
696: // Lower the font of the key
697: $key = strtolower($key);
698:
699: // Trim the value of extra space
700: $value = trim($value);
701:
702: // Switch and offload items as needed
703: switch ($key) {
704: case 'client':
705: $this->processPlayer($value, $playerFields, $result);
706: break;
707:
708: case 'channel':
709: $this->processChannel($value, $channelFields, $result);
710: break;
711:
712: // Find the number of fields for the channels
713: case 'channelfields':
714: $channelFields = count(explode(',', $value));
715: break;
716:
717: // Find the number of fields for the players
718: case 'clientfields':
719: $playerFields = count(explode(',', $value));
720: break;
721:
722: // By default we just add they key as an item
723: default:
724: $result->add($key, Str::isoToUtf8($value));
725: break;
726: }
727: }
728: }
729:
730: unset($decrypted, $line, $lines, $colon_pos, $key, $value);
731:
732: return $result->fetch();
733: }
734:
735: // Internal methods
736:
737: /**
738: * Decrypt the incoming packets
739: *
740: * @codeCoverageIgnore
741: *
742: * @param array $packets
743: * @return string
744: * @throws \GameQ\Exception\Protocol
745: */
746: protected function decryptPackets(array $packets = [])
747: {
748: // This will be returned
749: $decrypted = [];
750:
751: foreach ($packets as $packet) {
752: // Header :
753: $header = substr($packet, 0, 20);
754:
755: $header_items = [];
756:
757: $header_key = unpack("n1", $header);
758:
759: $key = array_shift($header_key);
760:
761: $chars = unpack("C*", substr($header, 2));
762:
763: $a1 = $key & 0xFF;
764: $a2 = $key >> 8;
765:
766: if ($a1 == 0) {
767: throw new Exception(__METHOD__ . ": Header key is invalid");
768: }
769:
770: $table = $this->head_encrypt_table;
771:
772: $characterCount = count($chars);
773:
774: $key = 0;
775: for ($index = 1; $index <= $characterCount; $index++) {
776: $chars[$index] -= ($table[$a2] + (($index - 1) % 5)) & 0xFF;
777: $a2 = ($a2 + $a1) & 0xFF;
778: if (($index % 2) == 0) {
779: $short_array = unpack("n1", pack("C2", $chars[$index - 1], $chars[$index]));
780: $header_items[$key] = $short_array[1];
781: ++$key;
782: }
783: }
784:
785: $header_items = array_combine([
786: 'zero',
787: 'cmd',
788: 'id',
789: 'totlen',
790: 'len',
791: 'totpck',
792: 'pck',
793: 'datakey',
794: 'crc',
795: ], $header_items);
796:
797: // Check to make sure the number of packets match
798: if ($header_items['totpck'] != count($packets)) {
799: throw new Exception(__METHOD__ . ": Too few packets received");
800: }
801:
802: // Data :
803: $table = $this->data_encrypt_table;
804: $a1 = $header_items['datakey'] & 0xFF;
805: $a2 = $header_items['datakey'] >> 8;
806:
807: if ($a1 == 0) {
808: throw new Exception(__METHOD__ . ": Data key is invalid");
809: }
810:
811: $chars = unpack("C*", substr($packet, 20));
812: $data = "";
813: $characterCount = count($chars);
814:
815: for ($index = 1; $index <= $characterCount; $index++) {
816: $chars[$index] -= ($table[$a2] + (($index - 1) % 72)) & 0xFF;
817: $a2 = ($a2 + $a1) & 0xFF;
818: $data .= chr($chars[$index]);
819: }
820: //@todo: Check CRC ???
821: $decrypted[$header_items['pck']] = $data;
822: }
823:
824: // Return the decrypted packets as one string
825: return implode('', $decrypted);
826: }
827:
828: /**
829: * Process the channel listing
830: *
831: * @param string $data
832: * @param int $fieldCount
833: * @param \GameQ\Result $result
834: * @return void
835: */
836: protected function processChannel($data, $fieldCount, Result &$result)
837: {
838: // Split the items on the comma
839: $items = explode(",", $data, $fieldCount);
840:
841: // Iterate over the items for this channel
842: foreach ($items as $item) {
843: // Split the key=value pair
844: list($key, $value) = explode("=", $item, 2);
845:
846: $result->addTeam(strtolower($key), Str::isoToUtf8($value));
847: }
848: }
849:
850: /**
851: * Process the user listing
852: *
853: * @param string $data
854: * @param int $fieldCount
855: * @param \GameQ\Result $result
856: * @return void
857: */
858: protected function processPlayer($data, $fieldCount, Result &$result)
859: {
860: // Split the items on the comma
861: $items = explode(",", $data, $fieldCount);
862:
863: // Iterate over the items for this player
864: foreach ($items as $item) {
865: // Split the key=value pair
866: list($key, $value) = explode("=", $item, 2);
867:
868: $result->addPlayer(strtolower($key), Str::isoToUtf8($value));
869: }
870: }
871: }
872: