mirror of
https://github.com/gamedig/node-gamedig.git
synced 2024-11-18 17:50:37 +01:00
716 lines
15 KiB
Text
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 );
|
|
}
|
|
|