/* * qstat 2.9 * by Steve Jankowski * * Doom3 / Quake4 protocol * Copyright 2005 Ludwig Nussel * * Licensed under the Artistic License, see LICENSE.txt for license terms */ #include #ifndef _WIN32 #include #endif #include #include #include "qstat.h" #include "debug.h" #include "assert.h" static const char doom3_master_query[] = "\xFF\xFFgetServers\x00\x00\x00\x00\x00\x00"; // version ^^^^^^^^^^^^^^^^ // null terminated mod string ^^^^ // filterbyte ^^^^ static const char quake4_master_query[] = "\xFF\xFFgetServers\x00\x00\x00\x00\x00\x00\x00\x00"; // version ^^^^^^^^^^^^^^^^ // null terminated mod string ^^^^ // null terminated player string ^^^^ // null terminated clan string ^^^^ // filterbyte ^^^^ static unsigned put_param_string(struct qserver* server, const char* paramname, char* buf, unsigned buflen, unsigned off) { char* val = get_param_value( server, paramname, NULL); if(val && strlen(val) < buflen-off-2) { strcpy(buf+off, val); off += strlen(val) + 1; } else { buf[off++] = '\0'; } return off; } static char* build_doom3_masterfilter(struct qserver* server, char* buf, unsigned* buflen, int q4) { int flen = 0; char *pkt, *r, *sep= ""; unsigned char b = 0; char *proto = server->query_arg; unsigned ver; unsigned off = 13; if(!proto) { if(q4) { *buflen = sizeof(quake4_master_query); return (char*)quake4_master_query; } else { *buflen = sizeof(doom3_master_query); return (char*)doom3_master_query; } } ver = (atoi(proto) & 0xFFFF) << 16; proto = strchr(proto, '.'); if(proto && *++proto) { ver |= (atoi(proto) & 0xFFFF); } if(q4) { ver |= 1 << 31; // third party flag } memcpy(buf, doom3_master_query, sizeof(doom3_master_query)); put_long_little(ver, buf+off); off += 4; off = put_param_string(server, "game", buf, *buflen, off); if(q4) { off = put_param_string(server, "player", buf, *buflen, off); off = put_param_string(server, "clan", buf, *buflen, off); } pkt = get_param_value( server, "status", NULL); r = pkt ; while ( pkt && sep ) { sep= strchr( r, ':'); if ( sep ) flen= sep-r; else flen= strlen(r); if ( strncmp( r, "password", flen) == 0) b |= 0x01; else if ( strncmp( r, "nopassword", flen) == 0) b |= 0x02; else if ( strncmp( r, "notfull", flen) == 0) b |= 0x04; else if ( strncmp( r, "notfullnotempty", flen) == 0) b |= 0x08; r= sep+1; } pkt = get_param_value( server, "gametype", NULL); if(pkt) { if ( strncmp( pkt, "dm", flen) == 0) b |= 0x10; else if ( strncmp( pkt, "tourney", flen) == 0) b |= 0x20; else if ( strncmp( pkt, "tdm", flen) == 0) b |= 0x30; } buf[off++] = (char)b; *buflen = off; return buf; } void send_doom3master_request_packet( struct qserver *server) { int rc = 0; int packet_len = -1; char* packet = NULL; char query_buf[4096] = {0}; server->next_player_info = NO_PLAYER_INFO; packet_len = sizeof(query_buf); packet = build_doom3_masterfilter(server, query_buf, (unsigned*)&packet_len, 0); rc= send( server->fd, packet, packet_len, 0); if ( rc == SOCKET_ERROR) { perror( "send"); } if ( server->retry1 == n_retries) { gettimeofday( &server->packet_time1, NULL); server->n_requests++; } else { server->n_retries++; } server->retry1--; server->n_packets++; } void send_quake4master_request_packet( struct qserver *server) { int rc = 0; int packet_len = -1; char* packet = NULL; char query_buf[4096] = {0}; server->next_player_info = NO_PLAYER_INFO; packet_len = sizeof(query_buf); packet = build_doom3_masterfilter(server, query_buf, (unsigned*)&packet_len, 1); rc= send( server->fd, packet, packet_len, 0); if ( rc == SOCKET_ERROR) { perror( "send"); } if ( server->retry1 == n_retries) { gettimeofday( &server->packet_time1, NULL); server->n_requests++; } else { server->n_retries++; } server->retry1--; server->n_packets++; } static const char doom3_masterresponse[] = "\xFF\xFFservers"; void deal_with_doom3master_packet( struct qserver *server, char *rawpkt, int pktlen) { char* pkt, *dest; int len; server->ping_total+= time_delta( &packet_recv_time, &server->packet_time1); if ( pktlen < sizeof(doom3_masterresponse) + 6 // at least one server || (pktlen - sizeof(doom3_masterresponse)) % 6 || memcmp( doom3_masterresponse, rawpkt, sizeof(doom3_masterresponse) ) != 0 ) { server->server_name= SERVERERROR; server->master_pkt_len = 0; malformed_packet(server, "too short or invalid response"); cleanup_qserver( server, 1); return; } server->retry1 = 0; // received at least one packet so no need to retry pkt = rawpkt + sizeof(doom3_masterresponse); len = pktlen - sizeof(doom3_masterresponse); server->master_pkt = (char*)realloc( server->master_pkt, server->master_pkt_len + len); dest = server->master_pkt + server->master_pkt_len; server->master_pkt_len += len; while(len > 0) { memcpy(dest, pkt, 4 ); dest[4] = pkt[5]; dest[5] = pkt[4]; dest += 6; pkt += 6; len -= 6; } assert(len == 0); server->n_servers= server->master_pkt_len / 6; debug(2, "%d servers added", server->n_servers); } static const char doom3_inforesponse[] = "\xFF\xFFinfoResponse"; static unsigned MAX_DOOM3_ASYNC_CLIENTS = 32; static void _deal_with_doom3_packet( struct qserver *server, char *rawpkt, int pktlen, unsigned version ) { char *ptr = rawpkt; char *end = rawpkt + pktlen; int type = 0; unsigned num_players = 0; unsigned challenge = 0; unsigned protocolver = 0; char tmp[32]; server->n_servers++; if ( server->server_name == NULL) { server->ping_total += time_delta( &packet_recv_time, &server->packet_time1); } else { gettimeofday( &server->packet_time1, NULL); } // Check if correct reply if ( pktlen < sizeof(doom3_inforesponse) +4 +4 +1 || memcmp( doom3_inforesponse, ptr, sizeof(doom3_inforesponse)) != 0 ) { malformed_packet(server, "too short or invalid response"); cleanup_qserver( server, 1); return; } ptr += sizeof(doom3_inforesponse); challenge = swap_long_from_little(ptr); ptr += 4; protocolver = swap_long_from_little(ptr); ptr += 4; snprintf(tmp, sizeof(tmp), "%u.%u", protocolver >> 16, protocolver & 0xFFFF); debug(2, "challenge: 0x%08X, protocol: %s (0x%X)", challenge, tmp, protocolver); // Commented out until we have a better way to specify the expected version // This is due to prey demo requiring version 4 yet prey retail version 3 /* if( protocolver >> 16 != version ) { malformed_packet(server, "protocol version %u, expected %u", protocolver >> 16, version ); cleanup_qserver( server, 1); return; } */ server->protocol_version = protocolver; add_rule( server, "protocol", tmp, NO_FLAGS ); while ( ptr < end ) { // server info: // name value pairs null seperated // empty name && value signifies the end of section char *key, *val; unsigned keylen, vallen; key = ptr; ptr = memchr(ptr, '\0', end-ptr); if ( !ptr ) { malformed_packet( server, "no rule key" ); cleanup_qserver( server, 1); return; } keylen = ptr - key; val = ++ptr; ptr = memchr(ptr, '\0', end-ptr); if ( !ptr ) { malformed_packet( server, "no rule value" ); cleanup_qserver( server, 1); return; } vallen = ptr - val; ++ptr; debug( 2, "key:%s=%s:", key, val); if(keylen == 0 && vallen == 0) { type = 1; break; // end } // Lets see what we've got if ( 0 == strcasecmp( key, "si_name" ) ) { server->server_name = strdup( val ); } else if( 0 == strcasecmp( key, "fs_game" ) ) { server->game = strdup( val ); } #if 0 else if( 0 == strcasecmp( key, "si_version" ) ) { // format: // DOOM 1.0.1262.win-x86 Jul 8 2004 16:46:37 server->protocol_version = atoi( val+1 ); } #endif else if( 0 == strcasecmp( key, "si_map" ) ) { server->map_name = strdup( val ); } else if( 0 == strcasecmp( key, "si_maxplayers" ) ) { server->max_players = atoi( val ); } add_rule( server, key, val, NO_FLAGS ); } if ( type != 1 ) { // no more info should be player headers here as we // requested it malformed_packet( server, "player info missing" ); cleanup_qserver( server, 1); return; } // now each player details while( ptr < end ) { struct player *player; char *val; unsigned char player_id = *ptr++; short prediction = 0; unsigned rate = 0; if(player_id == MAX_DOOM3_ASYNC_CLIENTS) break; /* id's are not steady if(player_id != num_players) { malformed_packet(server, "unexpected player id"); cleanup_qserver( server, 1); return; } */ if ( ptr + 7 > end ) // 2 pred + 4 rate + empty player name ('\0') { // run off the end and shouldnt have malformed_packet( server, "player info too short" ); cleanup_qserver( server, 1); return; } player = add_player( server, player_id ); player->score = 0; player->frags = 0; prediction = swap_short_from_little(ptr); ptr += 2; player->ping = prediction; // seems to be ping rate = swap_long_from_little(ptr); ptr += 4; { char buf[16]; snprintf(buf, sizeof(buf), "%u", rate); player_add_info(player, "rate", buf, NO_FLAGS); } val = ptr; ptr = memchr(ptr, '\0', end-ptr); if ( !ptr ) { malformed_packet( server, "player name not null terminated" ); cleanup_qserver( server, 1); return; } ++ptr; player->name = strdup( val ); if( 2 == version ) { val = ptr; ptr = memchr(ptr, '\0', end-ptr); if ( !ptr ) { malformed_packet( server, "player clan not null terminated" ); cleanup_qserver( server, 1); return; } ++ptr; player->tribe_tag = strdup( val ); } debug( 2, "Player[%d] = %s, prediction %hu, rate %u, id %hhu, clan %s", num_players, player->name, prediction, rate, player_id, player->tribe_tag); ++num_players; } if(end - ptr >= 4) { snprintf(tmp, sizeof(tmp), "0x%X", swap_long_from_little(ptr)); add_rule( server, "osmask", tmp, NO_FLAGS ); debug( 2, "osmask %s", tmp); ptr += 4; } else { malformed_packet(server, "osmask missing"); } #if 0 if(end - ptr) { malformed_packet(server, "%ld byes left", end-ptr); } #endif server->num_players = num_players; cleanup_qserver( server, 1 ); return; } void deal_with_doom3_packet( struct qserver *server, char *rawpkt, int pktlen) { _deal_with_doom3_packet( server, rawpkt, pktlen, 1 ); } void deal_with_quake4_packet( struct qserver *server, char *rawpkt, int pktlen) { _deal_with_doom3_packet( server, rawpkt, pktlen, 2 ); } void deal_with_prey_demo_packet( struct qserver *server, char *rawpkt, int pktlen ) { _deal_with_doom3_packet( server, rawpkt, pktlen, 4 ); } void deal_with_prey_packet( struct qserver *server, char *rawpkt, int pktlen ) { _deal_with_doom3_packet( server, rawpkt, pktlen, 3 ); }