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 <sys/types.h>
#ifndef _WIN32
#include <netinet/in.h>
#include <sys/socket.h>
#endif
#include <stdlib.h>
#include <stdio.h>

#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 );
}

