LICENSE: The Artistic License 2.0 /* * qstat.h * by Steve Jankowski * steve@qstat.org * http://www.qstat.org * * Copyright 1996,1997,1998,1999,2000,2001,2002 by Steve Jankowski */ #define HAZE_BASIC_INFO 0x01 #define HAZE_GAME_RULES 0x02 #define HAZE_PLAYER_INFO 0x04 #define HAZE_TEAM_INFO 0x08 // Format: // 1 - 8: Query Request // 9 - 12: Query Header // 13: Query ID // Query ID is made up of the following // 0x01: Basic Info // 0x02: Game Rules // 0x03: Player Information // 0x04: Team Information unsigned char haze_status_query[] = { 'f', 'r', 'd', 'q', 'u', 'e', 'r', 'y', 0x10,0x20,0x30,0x40, 0x0A }; // Format: // 1 - 8: Query Request // 9 - 12: Query Header // 13: Query ID // Query ID is made up of the following // 0x01: Basic Info // 0x02: Game Rules // 0x03: Player Information // 0x04: Team Information unsigned char haze_player_query[] = { 'f', 'r', 'd', 'q', 'u', 'e', 'r', 'y', 0x10,0x20,0x30,0x40, 0x03 }; { /* HAZE PROTOCOL */ HAZE_SERVER, /* id */ "HAZES", /* type_prefix */ "hazes", /* type_string */ "-hazes", /* type_option */ "Haze Protocol", /* game_name */ 0, /* master */ 0, /* default_port */ 0, /* port_offset */ TF_SINGLE_QUERY, /* flags */ "gametype", /* game_rule */ "HAZE", /* template_var */ (char*) &haze_status_query, /* status_packet */ sizeof( haze_status_query), /* status_len */ (char*) &haze_player_query, /* player_packet */ sizeof( haze_player_query), /* player_len */ NULL, /* rule_packet */ 0, /* rule_len */ NULL, /* master_packet */ 0, /* master_len */ NULL, /* master_protocol */ NULL, /* master_query */ display_gs2_player_info, /* display_player_func */ display_server_rules, /* display_rule_func */ raw_display_gs2_player_info, /* display_raw_player_func */ raw_display_server_rules, /* display_raw_rule_func */ xml_display_player_info, /* display_xml_player_func */ xml_display_server_rules, /* display_xml_rule_func */ send_haze_request_packet, /* status_query_func */ NULL, /* rule_query_func */ NULL, /* player_query_func */ deal_with_haze_packet, /* packet_func */ }, /* * qstat 2.8 * by Steve Jankowski * * New Haze query protocol * Copyright 2005 Steven Hartland * * Licensed under the Artistic License, see LICENSE.txt for license terms * */ #include #ifndef _WIN32 #include #include #endif #include #include #include "debug.h" #include "qstat.h" #include "packet_manip.h" // Format: // 1 - 8: Challenge Request / Response char haze_challenge[] = { 'f', 'r', 'd', 'c', '_', '_', '_', '_' }; int process_haze_packet( struct qserver *server ); // Player headers #define PLAYER_NAME_HEADER 1 #define PLAYER_SCORE_HEADER 2 #define PLAYER_DEATHS_HEADER 3 #define PLAYER_PING_HEADER 4 #define PLAYER_KILLS_HEADER 5 #define PLAYER_TEAM_HEADER 6 #define PLAYER_OTHER_HEADER 7 // Team headers #define TEAM_NAME_HEADER 1 #define TEAM_OTHER_HEADER 2 // Challenge response algorithum // Before sending a qr2 query (type 0x00) the client must first send a // challenge request (type 0x09). The host will respond with the same // packet type containing a string signed integer. // // Once the challenge is received the client should convert the string to a // network byte order integer and embed it in the keys query. // // Example: // // challenge request: [0xFE][0xFD][0x09][0x.. 4-byte-instance] // challenge response: [0x09][0x.. 4-byte-instance]["-1287574694"] // query: [0xFE][0xFD][0x00][0x.. 4-byte-instance][0xb3412b5a "-1287574694"] // query_status_t deal_with_haze_packet( struct qserver *server, char *rawpkt, int pktlen ) { char *ptr = rawpkt; unsigned int pkt_id; unsigned short len; unsigned char pkt_max, pkt_index; debug( 2, "packet..." ); if ( pktlen < 8 ) { // invalid packet malformed_packet( server, "too short" ); return PKT_ERROR; } if ( 0 == strncmp( ptr, "frdcr", 5 ) ) { // challenge response ptr += 8; server->challenge = 1; // Correct the stats due to two phase protocol server->retry1++; server->n_packets--; if ( server->retry1 == n_retries || server->flags & FLAG_BROADCAST ) { //server->n_requests--; } else { server->n_retries--; } return send_haze_request_packet( server ); } if ( pktlen < 12 ) { // invalid packet malformed_packet( server, "too short" ); return PKT_ERROR; } 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); } // Query version ID ptr += 4; // Could check the header here should // match the 4 byte id sent memcpy( &pkt_id, ptr, 4 ); ptr += 4; // Max plackets pkt_max = ((unsigned char)*ptr); ptr++; // Packet ID pkt_index = ((unsigned char)*ptr); ptr++; // Query Length //len = (unsigned short)ptr[0] | ((unsigned short)ptr[1] << 8); //len = swap_short_from_little( ptr ); debug( 1, "%04hx, %04hx", (unsigned short)ptr[0], ((unsigned short)ptr[1] << 8) ); //len = (unsigned short)(unsigned short)ptr[0] | ((unsigned short)ptr[1] << 8); // TODO: fix this crap memcpy( &len, ptr+1, 1 ); //memcpy( &len+1, ptr, 1 ); //memcpy( &len, ptr, 2 ); ptr += 2; debug( 1, "pkt_index = %d, pkt_max = %d, len = %d", pkt_index, pkt_max, len ); if ( 0 != pkt_max ) { // not a single packet response or callback debug( 2, "pkt_max %d", pkt_max ); if ( 0 == pkt_index ) { // to prevent reprocessing when we get the call back // override the packet flag so it looks like a single // packet response rawpkt[8] = '\0'; } // add the packet recalcing maxes if ( ! add_packet( server, pkt_id, pkt_index, pkt_max, pktlen, rawpkt, 1 ) ) { // fatal error e.g. out of memory return MEM_ERROR; } // combine_packets will call us recursively return combine_packets( server ); } // if we get here we have what should be a full packet return process_haze_packet( server ); } query_status_t deal_with_haze_status( struct qserver *server, char *rawpkt, int pktlen ) { char *pkt = rawpkt; int len; debug( 1, "status packet" ); // Server name server->server_name = strdup( pkt ); pkt += strlen( pkt ) + 1; // gametype add_rule( server, "gametype", pkt, NO_FLAGS ); pkt += strlen( pkt ) + 1; // map len = strlen( pkt ); // remove .res from map names if ( 0 == strncmp( pkt + len - 4, ".res", 4 ) ) { *(pkt + len - 4) = '\0'; } server->map_name = strdup( pkt ); pkt += len + 1; // num players server->num_players = atoi( pkt ); pkt += strlen( pkt ) + 1; // max_players server->max_players = atoi( pkt ); pkt += strlen( pkt ) + 1; // hostport change_server_port( server, atoi( pkt ), 0 ); pkt += strlen( pkt ) + 1; return DONE_FORCE; } int process_haze_packet( struct qserver *server ) { unsigned char state = 0; unsigned char no_players = 0; unsigned char total_players = 0; unsigned char no_teams = 0; unsigned char total_teams = 0; int pkt_index = 0; SavedData *fragment; debug( 2, "processing packet..." ); while ( NULL != ( fragment = get_packet_fragment( pkt_index++ ) ) ) { int pktlen = fragment->datalen; char *ptr = fragment->data; char *end = ptr + pktlen; debug( 2, "processing fragment[%d]...", fragment->pkt_index ); // check we have a full header if ( pktlen < 12 ) { // invalid packet malformed_packet( server, "too short" ); return PKT_ERROR; } // skip over the header //server->protocol_version = atoi( val+1 ); ptr += 12; // 4 * null's signifies the end of a section // Basic Info while ( 0 == state && ptr < end ) { // name value pairs null seperated char *var, *val; int var_len, val_len; if ( ptr+4 <= end && 0x00 == ptr[0] && 0x00 == ptr[1] && 0x00 == ptr[2] && 0x00 == ptr[3] ) { // end of rules state++; ptr += 4; break; } var = ptr; var_len = strlen( var ); ptr += var_len + 1; if ( ptr + 1 > end ) { malformed_packet( server, "no basic value" ); return PKT_ERROR; } val = ptr; val_len = strlen( val ); ptr += val_len + 1; debug( 2, "var:%s (%d)=%s (%d)\n", var, var_len, val, val_len ); // Lets see what we've got if ( 0 == strcmp( var, "serverName" ) ) { server->server_name = strdup( val ); } else if( 0 == strcmp( var, "map" ) ) { // remove .res from map names if ( 0 == strncmp( val + val_len - 4, ".res", 4 ) ) { *(val + val_len - 4) = '\0'; } server->map_name = strdup( val ); } else if( 0 == strcmp( var, "maxPlayers" ) ) { server->max_players = atoi( val ); } else if( 0 == strcmp( var, "currentPlayers" ) ) { server->num_players = no_players = atoi( val ); } else { add_rule( server, var, val, NO_FLAGS ); } } // rules while ( 1 == state && ptr < end ) { // name value pairs null seperated char *var, *val; int var_len, val_len; if ( ptr+4 <= end && 0x00 == ptr[0] && 0x00 == ptr[1] && 0x00 == ptr[2] && 0x00 == ptr[3] ) { // end of basic state++; ptr += 4; break; } var = ptr; var_len = strlen( var ); ptr += var_len + 1; if ( ptr + 1 > end ) { malformed_packet( server, "no basic value" ); return PKT_ERROR; } val = ptr; val_len = strlen( val ); ptr += val_len + 1; debug( 2, "var:%s (%d)=%s (%d)\n", var, var_len, val, val_len ); // add the rule add_rule( server, var, val, NO_FLAGS ); } // players while ( 2 == state && ptr < end ) { // first we have the header char *header = ptr; int head_len = strlen( header ); ptr += head_len + 1; if ( ptr+2 <= end && 0x00 == ptr[0] && 0x00 == ptr[1] ) { // end of player headers state++; ptr += 2; break; } if ( 0 == head_len ) { // no more info debug( 3, "All done" ); return DONE_FORCE; } debug( 2, "player header '%s'", header ); if ( ptr > end ) { malformed_packet( server, "no details for header '%s'", header ); return PKT_ERROR; } } while ( 3 == state && ptr < end ) { char *header = ptr; int head_len = strlen( header ); int header_type; // the next byte is the starting number total_players = *ptr++; if ( 0 == strcmp( header, "player_" ) || 0 == strcmp( header, "name_" ) ) { header_type = PLAYER_NAME_HEADER; } else if ( 0 == strcmp( header, "score_" ) ) { header_type = PLAYER_SCORE_HEADER; } else if ( 0 == strcmp( header, "deaths_" ) ) { header_type = PLAYER_DEATHS_HEADER; } else if ( 0 == strcmp( header, "ping_" ) ) { header_type = PLAYER_PING_HEADER; } else if ( 0 == strcmp( header, "kills_" ) ) { header_type = PLAYER_KILLS_HEADER; } else if ( 0 == strcmp( header, "team_" ) ) { header_type = PLAYER_TEAM_HEADER; } else { header_type = PLAYER_OTHER_HEADER; } while( ptr < end ) { // now each player details // add the player struct player *player; char *val; int val_len; // check for end of this headers player info if ( 0x00 == *ptr ) { debug( 3, "end of '%s' detail", header ); ptr++; // Note: can't check ( total_players != no_players ) here as we may have more packets if ( ptr < end && 0x00 == *ptr ) { debug( 3, "end of players" ); // end of all player headers / detail state = 2; ptr++; } break; } player = get_player_by_number( server, total_players ); if ( NULL == player ) { player = add_player( server, total_players ); } if ( ptr >= end ) { malformed_packet( server, "short player detail" ); return PKT_ERROR; } val = ptr; val_len = strlen( val ); ptr += val_len + 1; debug( 2, "Player[%d][%s]=%s\n", total_players, header, val ); // lets see what we got switch( header_type ) { case PLAYER_NAME_HEADER: player->name = strdup( val ); break; case PLAYER_SCORE_HEADER: player->score = atoi( val ); break; case PLAYER_DEATHS_HEADER: player->deaths = atoi( val ); break; case PLAYER_PING_HEADER: player->ping = atoi( val ); break; case PLAYER_KILLS_HEADER: player->frags = atoi( val ); break; case PLAYER_TEAM_HEADER: player->team = atoi( val ); break; case PLAYER_OTHER_HEADER: default: if ( '_' == header[head_len-1] ) { header[head_len-1] = '\0'; player_add_info( player, header, val, NO_FLAGS ); header[head_len-1] = '_'; } else { player_add_info( player, header, val, NO_FLAGS ); } break; } total_players++; if ( total_players > no_players ) { malformed_packet( server, "to many players %d > %d", total_players, no_players ); return PKT_ERROR; } } } if ( 3 == state ) { no_teams = (unsigned char)*ptr; ptr++; debug( 2, "No teams:%d\n", no_teams ); state = 3; } while ( 4 == state && ptr < end ) { // first we have the header char *header = ptr; int head_len = strlen( header ); int header_type; ptr += head_len + 1; if ( 0 == head_len ) { // no more info debug( 3, "All done" ); return DONE_FORCE; } debug( 2, "team header '%s'", header ); if ( 0 == strcmp( header, "team_t" ) ) { header_type = TEAM_NAME_HEADER; } else { header_type = TEAM_OTHER_HEADER; } // the next byte is the starting number total_teams = *ptr++; while( ptr < end ) { // now each teams details char *val; int val_len; char rule[512]; if ( ptr >= end ) { malformed_packet( server, "short team detail" ); return PKT_ERROR; } val = ptr; val_len = strlen( val ); ptr += val_len + 1; debug( 2, "Team[%d][%s]=%s\n", total_teams, header, val ); // lets see what we got switch ( header_type ) { case TEAM_NAME_HEADER: // BF being stupid again teams 1 based instead of 0 players_set_teamname( server, total_teams + 1, val ); // N.B. yes no break case TEAM_OTHER_HEADER: default: // add as a server rule sprintf( rule, "%s%d", header, total_teams ); add_rule( server, rule, val, NO_FLAGS ); break; } total_teams++; if ( 0x00 == *ptr ) { // end of this headers teams ptr++; break; } } } } return DONE_FORCE; } query_status_t send_haze_request_packet( struct qserver *server ) { char *packet; char query_buf[128]; size_t len; unsigned char required = HAZE_BASIC_INFO; if ( get_server_rules ) { required |= HAZE_GAME_RULES; server->flags |= TF_PLAYER_QUERY; } if ( get_player_info ) { required |= HAZE_PLAYER_INFO; required |= HAZE_TEAM_INFO; server->flags |= TF_RULES_QUERY; } server->flags |= TF_STATUS_QUERY; if ( server->challenge ) { // we've recieved a challenge response, send the query + challenge id len = sprintf( query_buf, "frdquery%c%c%c%c%c", (unsigned char)(server->challenge >> 24), (unsigned char)(server->challenge >> 16), (unsigned char)(server->challenge >> 8), (unsigned char)(server->challenge >> 0), required ); packet = query_buf; } else { // Either basic v3 protocol or challenge request packet = haze_challenge; len = sizeof( haze_challenge ); } return send_packet( server, packet, len ); }