node-gamedig/reference/haze/qstat.txt
2014-10-29 02:02:03 -05:00

716 lines
15 KiB
Text

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