1 /*
2 * qstat
3 * by Steve Jankowski
4 *
5 * New Half-Life2 query protocol
6 * Copyright 2005 Ludwig Nussel
7 *
8 * Licensed under the Artistic License, see LICENSE.txt for license terms
9 *
10 */
11
12 #include <sys/types.h>
13 #ifndef _WIN32
14 #include <sys/socket.h>
15 #include <arpa/inet.h>
16 #endif
17 #include <stdlib.h>
18 #include <stdio.h>
19
20 #include "debug.h"
21 #include "qstat.h"
22 #include "packet_manip.h"
23
24 #define A2S_GETCHALLENGE "\xFF\xFF\xFF\xFF\x57"
25 #define A2S_CHALLENGERESPONSE 0x41
26 #define A2S_INFO "\xFF\xFF\xFF\xFF\x54Source Engine Query"
27 #define A2S_INFORESPONSE_HL1 0x6D
28 #define A2S_INFORESPONSE_HL2 0x49
29 #define A2S_PLAYER "\xFF\xFF\xFF\xFF\x55"
30 #define A2S_PLAYERRESPONSE 0x44
31 #define A2S_PLAYER_INVALID_CHALLENGE "\xFF\xFF\xFF\xFF\x55\xFF\xFF\xFF\xFF"
32 #define A2S_RULES "\xFF\xFF\xFF\xFF\x56"
33 #define A2S_RULESRESPONSE 0x45
34
35 struct a2s_status {
36 unsigned sent_challenge : 1;
37 unsigned have_challenge : 1;
38 unsigned sent_info : 1;
39 unsigned have_info : 1;
40 unsigned sent_player : 1;
41 unsigned have_player : 1;
42 unsigned sent_rules : 1;
43 unsigned have_rules : 1;
44 unsigned challenge;
45 unsigned char type;
46 };
47
48 query_status_t
send_a2s_request_packet(struct qserver * server)49 send_a2s_request_packet(struct qserver *server)
50 {
51 struct a2s_status *status = (struct a2s_status *)server->master_query_tag;
52
53 debug(3, "sending info query");
54 if (qserver_send_initial(server, A2S_INFO, sizeof(A2S_INFO)) == SOCKET_ERROR) {
55 return (DONE_FORCE);
56 }
57
58 status->sent_info = 1;
59 status->type = 0;
60
61 if (get_server_rules || get_player_info) {
62 server->next_rule = ""; // trigger calling send_a2s_rule_request_packet
63 }
64 return (INPROGRESS);
65 }
66
67
68 query_status_t
send_a2s_rule_request_packet(struct qserver * server)69 send_a2s_rule_request_packet(struct qserver *server)
70 {
71 struct a2s_status *status = (struct a2s_status *)server->master_query_tag;
72 debug(3, "rule request");
73
74 if (!get_server_rules && !get_player_info && status->have_info) {
75 debug(3, "force done");
76 return (DONE_FORCE);
77 }
78
79 if (server->retry1 < 0) {
80 debug(3, "too may retries");
81 return (DONE_FORCE);
82 }
83
84 while (1) {
85 if (!status->have_challenge) {
86 debug(3, "sending challenge");
87
88 // Challenge Request was broken so instead we use a player request with an invalid
89 // challenge of -1 (0xFFFFFFFF) which prompts the server to send a valid challenge
90 // This was fixed as of the update 2009-08-26 but then subsequently broken again.
91 //if (SOCKET_ERROR == qserver_send_initial(server, A2S_GETCHALLENGE, sizeof(A2S_GETCHALLENGE)-1)) {
92 char buf[sizeof(A2S_PLAYER) - 1 + 4] = A2S_PLAYER;
93 // We use a challenge of -1 to ensure compatibility with 3rd party implementations
94 // as that's whats documented: https://developer.valvesoftware.com/wiki/Server_queries#Request_Format_5
95 status->challenge = -1;
96 memcpy(buf + sizeof(A2S_PLAYER) - 1, &status->challenge, 4);
97 if (qserver_send_initial(server, buf, sizeof(buf)) == SOCKET_ERROR) {
98 return (SOCKET_ERROR);
99 }
100 status->sent_challenge = 1;
101 break;
102 } else if (status->sent_info && !status->have_info) {
103 // Need to resend info due to enhanced DDoS protection
104 // See: https://steamcommunity.com/discussions/forum/14/2974028351344359625/
105 char buf[sizeof(A2S_INFO) + 4] = A2S_INFO;
106 memcpy(buf + sizeof(A2S_INFO), &status->challenge, 4);
107 debug(3, "sending info query with challenge");
108 if (qserver_send_initial(server, buf, sizeof(buf)) == SOCKET_ERROR) {
109 return (SOCKET_ERROR);
110 }
111 break;
112 } else if (get_server_rules && !status->have_rules) {
113 char buf[sizeof(A2S_RULES) - 1 + 4] = A2S_RULES;
114 memcpy(buf + sizeof(A2S_RULES) - 1, &status->challenge, 4);
115 debug(3, "sending rule query");
116 if (qserver_send_initial(server, buf, sizeof(buf)) == SOCKET_ERROR) {
117 return (SOCKET_ERROR);
118 }
119 status->sent_rules = 1;
120 break;
121 } else if (get_player_info && !status->have_player) {
122 char buf[sizeof(A2S_PLAYER) - 1 + 4] = A2S_PLAYER;
123 memcpy(buf + sizeof(A2S_PLAYER) - 1, &status->challenge, 4);
124 debug(3, "sending player query");
125 if (qserver_send_initial(server, buf, sizeof(buf)) == SOCKET_ERROR) {
126 return (SOCKET_ERROR);
127 }
128 status->sent_player = 1;
129 break;
130 } else {
131 debug(3, "timeout");
132 // we are probably called due to timeout, restart.
133 status->have_challenge = 0;
134 status->have_rules = 0;
135 }
136 }
137
138 return (INPROGRESS);
139 }
140
141
142 query_status_t
deal_with_a2s_packet(struct qserver * server,char * rawpkt,int pktlen)143 deal_with_a2s_packet(struct qserver *server, char *rawpkt, int pktlen)
144 {
145 struct a2s_status *status = (struct a2s_status *)server->master_query_tag;
146 char *pkt = rawpkt;
147 char buf[16];
148 char *str;
149 unsigned cnt;
150
151 if (server->server_name == NULL) {
152 server->ping_total += time_delta(&packet_recv_time, &server->packet_time1);
153 server->n_requests++;
154 }
155
156 if (pktlen < 5) {
157 goto out_too_short;
158 }
159
160 if (0 == memcmp(pkt, "\xFE\xFF\xFF\xFF", 4)) {
161 // fragmented packet
162 unsigned char pkt_index, pkt_max;
163 unsigned int pkt_id = 1;
164 SavedData *sdata;
165
166 if (pktlen < 9) {
167 goto out_too_short;
168 }
169
170 pkt += 4;
171
172 // format:
173 // int sequenceNumber
174 // byte packetId
175 // packetId format:
176 // bits 0 - 3 = packets position in the sequence ( 0 .. N - 1 )
177 // bits 4 - 7 = total number of packets
178
179 // sequenceId
180 memcpy(&pkt_id, pkt, 4);
181 debug(3, "sequenceId: %d", pkt_id);
182 pkt += 4;
183
184 // packetId
185 if ((1 == status->type) || (200 > server->protocol_version)) {
186 // HL1 format
187 // The lower four bits represent the number of packets (2 to 15) and
188 // the upper four bits represent the current packet starting with 0
189 pkt_max = ((unsigned char)*pkt) & 15;
190 pkt_index = ((unsigned char)*pkt) >> 4;
191 debug(3, "packetid[1]: 0x%hhx => idx: %hhu, max: %hhu", *pkt, pkt_index, pkt_max);
192 pkt++;
193 pktlen -= 9;
194 } else if (2 == status->type) {
195 // HL2 format
196 // The next two bytes are:
197 // 1. the max packets sent ( byte )
198 // 2. the index of this packet starting from 0 ( byte )
199 // 3. Size of the split ( short )
200 if (pktlen < 10) {
201 goto out_too_short;
202 }
203 pkt_max = ((unsigned char)*pkt);
204 pkt_index = ((unsigned char)*(pkt + 1));
205 debug(3, "packetid[2]: 0x%hhx => idx: %hhu, max: %hhu", *pkt, pkt_index, pkt_max);
206 pkt += 4;
207 pktlen -= 12;
208 } else {
209 malformed_packet(server, "Unable to determine packet format");
210 return (PKT_ERROR);
211 }
212
213 // pkt_max is the total number of packets expected
214 // pkt_index is a bit mask of the packets received.
215
216 if (server->saved_data.data == NULL) {
217 sdata = &server->saved_data;
218 } else {
219 sdata = (SavedData *)calloc(1, sizeof(SavedData));
220 sdata->next = server->saved_data.next;
221 server->saved_data.next = sdata;
222 }
223
224 sdata->pkt_index = pkt_index;
225 sdata->pkt_max = pkt_max;
226 sdata->pkt_id = pkt_id;
227 sdata->datalen = pktlen;
228 sdata->data = (char *)malloc(sdata->datalen);
229 if (NULL == sdata->data) {
230 malformed_packet(server, "Out of memory");
231 return (MEM_ERROR);
232 }
233 memcpy(sdata->data, pkt, sdata->datalen);
234
235 // combine_packets will call us recursively
236 return (combine_packets(server));
237 } else if (0 != memcmp(pkt, "\xFF\xFF\xFF\xFF", 4)) {
238 malformed_packet(server, "invalid packet header");
239 return (PKT_ERROR);
240 }
241
242 pkt += 4;
243 pktlen -= 4;
244
245 pktlen -= 1;
246 debug(2, "A2S type = %x", *pkt);
247 switch (*pkt++) {
248 case A2S_CHALLENGERESPONSE:
249 if (pktlen < 4) {
250 goto out_too_short;
251 }
252 memcpy(&status->challenge, pkt, 4);
253 // do not count challenge as retry
254 if (!status->have_challenge && (server->retry1 != n_retries)) {
255 ++server->retry1;
256 if (server->n_retries) {
257 --server->n_retries;
258 }
259 }
260 status->have_challenge = 1;
261 debug(3, "challenge %x", status->challenge);
262 return (send_a2s_rule_request_packet(server));
263
264 case A2S_INFORESPONSE_HL1:
265 if (pktlen < 28) {
266 goto out_too_short;
267 }
268 status->type = 1;
269
270 // ip:port
271 str = memchr(pkt, '\0', pktlen);
272 if (!str) {
273 goto out_too_short;
274 }
275 //server->server_name = strdup(pkt);
276 pktlen -= str - pkt + 1;
277 pkt += str - pkt + 1;
278
279 // server name
280 str = memchr(pkt, '\0', pktlen);
281 if (!str) {
282 goto out_too_short;
283 }
284 server->server_name = strdup(pkt);
285 pktlen -= str - pkt + 1;
286 pkt += str - pkt + 1;
287
288 // map
289 str = memchr(pkt, '\0', pktlen);
290 if (!str) {
291 goto out_too_short;
292 }
293 server->map_name = strdup(pkt);
294 pktlen -= str - pkt + 1;
295 pkt += str - pkt + 1;
296
297 // mod dir
298 str = memchr(pkt, '\0', pktlen);
299 if (!str) {
300 goto out_too_short;
301 }
302 server->game = strdup(pkt);
303 add_rule(server, "gamedir", pkt, 0);
304 pktlen -= str - pkt + 1;
305 pkt += str - pkt + 1;
306
307 // mod description
308 str = memchr(pkt, '\0', pktlen);
309 if (!str) {
310 goto out_too_short;
311 }
312 add_rule(server, "gamename", pkt, 0);
313 pktlen -= str - pkt + 1;
314 pkt += str - pkt + 1;
315
316 if (pktlen < 7) {
317 goto out_too_short;
318 }
319
320 // num players
321 server->num_players = (unsigned char)pkt[0];
322
323 // max players
324 server->max_players = (unsigned char)pkt[1];
325
326 // version
327 sprintf(buf, "%hhu", pkt[2]);
328 add_rule(server, "version", buf, 0);
329
330 // dedicated
331 add_rule(server, "dedicated", pkt[3] == 'd' ? "1" : "0", 0);
332
333 // os
334 switch (pkt[4]) {
335 case 'l':
336 add_rule(server, "sv_os", "linux", 0);
337 break;
338
339 case 'w':
340 add_rule(server, "sv_os", "windows", 0);
341 break;
342
343 default:
344 buf[0] = pkt[4];
345 buf[1] = '\0';
346 add_rule(server, "sv_os", buf, 0);
347 }
348
349 // password
350 add_rule(server, "password", pkt[5] ? "1" : "0", 0);
351
352 pkt += 6;
353 pktlen -= 6;
354
355 // mod info
356 if (pkt[0]) {
357 pkt++;
358 pktlen--;
359
360 // mod URL
361 str = memchr(pkt, '\0', pktlen);
362 if (!str) {
363 goto out_too_short;
364 }
365 add_rule(server, "mod_url", strdup(pkt), 0);
366 pktlen -= str - pkt + 1;
367 pkt += str - pkt + 1;
368
369 // mod DL
370 str = memchr(pkt, '\0', pktlen);
371 if (!str) {
372 goto out_too_short;
373 }
374 add_rule(server, "mod_dl", strdup(pkt), 0);
375 pktlen -= str - pkt + 1;
376 pkt += str - pkt + 1;
377
378 // mod Empty
379 str = memchr(pkt, '\0', pktlen);
380 pktlen -= str - pkt + 1;
381 pkt += str - pkt + 1;
382
383 if (pktlen < 10) {
384 goto out_too_short;
385 }
386
387 // mod version
388 sprintf(buf, "%u", swap_long_from_little(pkt));
389 add_rule(server, "mod_ver", buf, 0);
390 pkt += 4;
391 pktlen -= 4;
392
393 // mod size
394 sprintf(buf, "%u", swap_long_from_little(pkt));
395 add_rule(server, "mod_size", buf, 0);
396 pkt += 4;
397 pktlen -= 4;
398
399 // svonly
400 add_rule(server, "mod_svonly", (*pkt) ? "1" : "0", 0);
401 pkt++;
402 pktlen--;
403
404 // cldll
405 add_rule(server, "mod_cldll", (*pkt) ? "1" : "0", 0);
406 pkt++;
407 pktlen--;
408 }
409
410 if (pktlen < 2) {
411 goto out_too_short;
412 }
413
414 // Secure
415 add_rule(server, "secure", *pkt ? "1" : "0", 0);
416 pkt++;
417 pktlen--;
418
419 // Bots
420 sprintf(buf, "%hhu", *pkt);
421 add_rule(server, "bots", buf, 0);
422 pkt++;
423 pktlen--;
424
425 status->have_info = 1;
426
427 server->retry1 = n_retries;
428
429 server->next_player_info = server->num_players;
430
431 break;
432
433 case A2S_INFORESPONSE_HL2:
434 if (pktlen < 1) {
435 goto out_too_short;
436 }
437 status->type = 2;
438 snprintf(buf, sizeof(buf), "%hhX", *pkt);
439 add_rule(server, "protocol", buf, 0);
440 ++pkt;
441 --pktlen;
442
443 // server name
444 str = memchr(pkt, '\0', pktlen);
445 if (!str) {
446 goto out_too_short;
447 }
448 server->server_name = strdup(pkt);
449 pktlen -= str - pkt + 1;
450 pkt += str - pkt + 1;
451
452 // map
453 str = memchr(pkt, '\0', pktlen);
454 if (!str) {
455 goto out_too_short;
456 }
457 server->map_name = strdup(pkt);
458 pktlen -= str - pkt + 1;
459 pkt += str - pkt + 1;
460
461 // mod
462 str = memchr(pkt, '\0', pktlen);
463 if (!str) {
464 goto out_too_short;
465 }
466 server->game = strdup(pkt);
467 add_rule(server, "gamedir", pkt, 0);
468 pktlen -= str - pkt + 1;
469 pkt += str - pkt + 1;
470
471 // description
472 str = memchr(pkt, '\0', pktlen);
473 if (!str) {
474 goto out_too_short;
475 }
476 add_rule(server, "gamename", pkt, 0);
477 pktlen -= str - pkt + 1;
478 pkt += str - pkt + 1;
479
480 if (pktlen < 9) {
481 goto out_too_short;
482 }
483
484 // pkt[0], pkt[1] steam appid
485 server->protocol_version = (unsigned short)*pkt;
486 server->num_players = (unsigned char)pkt[2];
487 server->max_players = (unsigned char)pkt[3];
488 // pkt[4] number of bots
489 sprintf(buf, "%hhu", pkt[4]);
490 add_rule(server, "bots", buf, 0);
491
492 add_rule(server, "dedicated", pkt[5] ? "1" : "0", 0);
493 if (pkt[6] == 'l') {
494 add_rule(server, "sv_os", "linux", 0);
495 } else if (pkt[6] == 'w') {
496 add_rule(server, "sv_os", "windows", 0);
497 } else {
498 buf[0] = pkt[6];
499 buf[1] = '\0';
500 add_rule(server, "sv_os", buf, 0);
501 }
502
503 if (pkt[7]) {
504 snprintf(buf, sizeof(buf), "%hhu", (unsigned char)pkt[7]);
505 add_rule(server, "password", buf, 0);
506 }
507
508 if (pkt[8]) {
509 snprintf(buf, sizeof(buf), "%hhu", (unsigned char)pkt[8]);
510 add_rule(server, "secure", buf, 0);
511 }
512
513 pkt += 9;
514 pktlen -= 9;
515
516 // version
517 str = memchr(pkt, '\0', pktlen);
518 if (!str) {
519 goto out_too_short;
520 }
521 add_rule(server, "version", pkt, 0);
522 pktlen -= str - pkt + 1;
523 pkt += str - pkt + 1;
524
525 // EDF
526 if (1 <= pktlen) {
527 unsigned char edf = *pkt;
528 debug(1, "EDF: 0x%02hhx", edf);
529 pkt++;
530 pktlen--;
531 if (edf & 0x80) {
532 // game port
533 unsigned short gameport;
534
535 if (pktlen < 2) {
536 goto out_too_short;
537 }
538 gameport = swap_short_from_little(pkt);
539 sprintf(buf, "%hu", gameport);
540 add_rule(server, "game_port", buf, 0);
541 change_server_port(server, gameport, 0);
542 pkt += 2;
543 pktlen -= 2;
544 }
545
546 if (edf & 0x10) {
547 // SteamId (long long)
548 if (pktlen < 8) {
549 goto out_too_short;
550 }
551 pkt += 8;
552 pktlen -= 8;
553 }
554
555 if (edf & 0x40) {
556 // spectator port
557 unsigned short spectator_port;
558 if (pktlen < 3) {
559 goto out_too_short;
560 }
561 spectator_port = swap_short_from_little(pkt);
562 sprintf(buf, "%hu", spectator_port);
563 add_rule(server, "spectator_port", buf, 0);
564 pkt += 2;
565 pktlen -= 2;
566
567 // spectator server name
568 str = memchr(pkt, '\0', pktlen);
569 if (!str) {
570 goto out_too_short;
571 }
572 add_rule(server, "spectator_server_name", pkt, 0);
573 pktlen -= str - pkt + 1;
574 pkt += str - pkt + 1;
575 }
576
577 if (edf & 0x20) {
578 // Keywords
579 str = memchr(pkt, '\0', pktlen);
580 if (!str) {
581 goto out_too_short;
582 }
583 add_rule(server, "game_tags", pkt, 0);
584 if (strncmp(pkt, "rust", 4) == 0) {
585 // Rust is comma seperated tags
586 char *keyword = strtok(pkt, ",");
587 while (keyword != NULL) {
588 if (strncmp(keyword, "cp", 2) == 0) {
589 // current players override
590 server->num_players = atoi(keyword + 2);
591 } else if (strncmp(keyword, "mp", 2) == 0) {
592 // max players override
593 server->max_players = atoi(keyword + 2);
594 }
595 keyword = strtok(NULL, ",");
596 }
597 }
598 pktlen -= str - pkt + 1;
599 pkt += str - pkt + 1;
600 }
601
602 if (edf & 0x01) {
603 // GameId (long long)
604 if (pktlen < 8) {
605 goto out_too_short;
606 }
607 pkt += 8;
608 pktlen -= 8;
609 }
610 }
611
612 status->have_info = 1;
613
614 server->retry1 = n_retries;
615
616 server->next_player_info = server->num_players;
617
618 break;
619
620 case A2S_RULESRESPONSE:
621
622 if (pktlen < 2) {
623 goto out_too_short;
624 }
625
626 cnt = (unsigned char)pkt[0] + ((unsigned char)pkt[1] << 8);
627 pktlen -= 2;
628 pkt += 2;
629
630 debug(3, "num_rules: %d", cnt);
631
632 for ( ; cnt && pktlen > 0; --cnt) {
633 char *key, *value;
634 str = memchr(pkt, '\0', pktlen);
635 if (!str) {
636 break;
637 }
638 key = pkt;
639 pktlen -= str - pkt + 1;
640 pkt += str - pkt + 1;
641
642 str = memchr(pkt, '\0', pktlen);
643 if (!str) {
644 break;
645 }
646 value = pkt;
647 pktlen -= str - pkt + 1;
648 pkt += str - pkt + 1;
649
650 add_rule(server, key, value, NO_FLAGS);
651 }
652
653 if (cnt) {
654 malformed_packet(server, "packet contains too few rules, missing %d", cnt);
655 server->missing_rules = 1;
656 }
657 if (pktlen) {
658 malformed_packet(server, "garbage at end of rules, %d bytes left", pktlen);
659 }
660
661 status->have_rules = 1;
662
663 server->retry1 = n_retries;
664
665 break;
666
667 case A2S_PLAYERRESPONSE:
668
669 if (pktlen < 1) {
670 goto out_too_short;
671 }
672
673 cnt = (unsigned char)pkt[0];
674 pktlen -= 1;
675 pkt += 1;
676
677 debug(3, "num_players: %d", cnt);
678
679 for ( ; cnt && pktlen > 0; --cnt) {
680 unsigned idx;
681 const char *name;
682 struct player *p;
683
684 idx = *pkt++;
685 --pktlen;
686
687 str = memchr(pkt, '\0', pktlen);
688 if (!str) {
689 break;
690 }
691 name = pkt;
692 pktlen -= str - pkt + 1;
693 pkt += str - pkt + 1;
694
695 if (pktlen < 8) {
696 goto out_too_short;
697 }
698
699 debug(3, "player index %d = %s", idx, name);
700 p = add_player(server, server->n_player_info);
701 if (p) {
702 p->name = strdup(name);
703 p->frags = swap_long_from_little(pkt);
704 p->connect_time = swap_float_from_little(pkt + 4);
705 }
706 pktlen -= 8;
707 pkt += 8;
708 }
709
710 if (pktlen) {
711 malformed_packet(server, "garbage at end of player info, %d bytes left", pktlen);
712 }
713
714 status->have_player = 1;
715 // Workaround broken implementations which don't send a challenge to a player request
716 // that hasn't seen a challenge yet. Without this we would end up in an infinite loop.
717 if (status->have_challenge == 0) {
718 if (show_errors) {
719 fprintf(stderr, "server has broken challenge so is a DDoS source!\n");
720 }
721 status->have_challenge = 1;
722 }
723
724 server->retry1 = n_retries;
725
726 break;
727
728 default:
729 malformed_packet(server, "invalid packet id %hhx", *--pkt);
730 return (PKT_ERROR);
731 }
732
733 if (
734 (!get_player_info || (get_player_info && status->have_player)) &&
735 (!get_server_rules || (get_server_rules && status->have_rules))
736 ) {
737 server->next_rule = NULL;
738 }
739
740 return (DONE_AUTO);
741
742 out_too_short:
743 malformed_packet(server, "packet too short");
744 return (PKT_ERROR);
745 }
746
747
748 // vim: sw=4 ts=4 noet
749