/*
 * qstat 1.4 beta
 * by Steve Jankowski
 * steve@activesw.com
 * http://www.activesw.com/people/steve/qstat.html
 *
 * Inspired by QuakePing by Len Norton
 *
 * Copyright 1996 by Steve Jankowski
 *
 * Permission granted to use for any purpose you desire as long as
 * you maintain this file prolog in the source code and you
 * derive no monetary benefit from use of this source or resulting
 * program.
 */

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/types.h>
#include <string.h>
#include <ctype.h>

#ifdef unix
#include <unistd.h>
#include <sys/socket.h>
#include <sys/param.h>
#include <sys/time.h>
#include <netinet/in.h>
#include <netdb.h>

#define INVALID_SOCKET -1
#define SOCKET_ERROR -1
#ifndef sgi
#ifndef linux
#define INADDR_NONE ~0
#endif
#endif
#endif

#ifdef _WIN32
#include <windows.h>

#define close(a) closesocket(a)
#endif

#define MAXFD			20
#define DEFAULT_PORT		26000
#define DEFAULT_RETRIES		3
#define DEFAULT_RETRY_INTERVAL	500		/* milli-seconds */
#define MAXIP 4

struct qserver {
    char *arg;
    char *host_name;
    struct in_addr ip[MAXIP];
    int fd;
    int port;
    int retry;

    char *server_name;
    char *address;
    char *map_name;
    int max_players;
    int num_players;
    struct qserver *next;
};

int hostname_lookup= 0;		/* set if -H was specified */

/* MODIFY HERE
 * Change this routine to display stats however you like.
 */
void
display_stats(
    struct qserver *server
)
{
    char name[100];
    sprintf( name, "\"%s\"", server->server_name);
    printf( "%-16s %10s map %s at %22s %d/%d players\n", 
	(hostname_lookup) ? server->host_name : server->arg,
	name, server->map_name,
	server->address, server->num_players, server->max_players);
}

#define PACKET_LEN 1600

char *DOWN= "DOWN";
char *SYSERROR= "ERROR";
char *TIMEOUT= "TIMEOUT";

unsigned char init_session[] = { 0x80, 0x00, 0x00, 0x0C, 0x02, 0x51,
                      0x55, 0x41, 0x4B, 0x45, 0x00, 0x01 };

struct qserver *servers= NULL;
int connected= 0;
int n_retries= DEFAULT_RETRIES;
int retry_interval= DEFAULT_RETRY_INTERVAL;

char *
strherror( int h_err)
{
    static char msg[100];
    switch (h_err)  {
    case HOST_NOT_FOUND:	return "host not found";
    case TRY_AGAIN:		return "try again";
    case NO_RECOVERY:		return "no recovery";
    case NO_ADDRESS:		return "no address";
    default:	sprintf( msg, "%d", h_err); return msg;
    }
}

int
add_qserver( char *arg)
{
    struct sockaddr_in addr;
    struct hostent *ent= NULL;
    struct hostent temp_ent;
    struct hostent *name_ent= NULL;
    struct qserver *server;
    int i, port= DEFAULT_PORT;
    char **a, *colon;
    char *h_addr_list[MAXIP];
    unsigned long ipaddr;
 
    colon= strchr( arg, ':');
    if ( colon != NULL)  {
	if ( sscanf( colon+1, "%d", &port) != 1)  {
	    fprintf( stderr, "Could not parse port from \"%s\", using %d\n",
		arg, DEFAULT_PORT);
	    port= DEFAULT_PORT;
	}
	*colon= '\0';
    }

    ipaddr= inet_addr(arg);
    if ( ipaddr == INADDR_NONE)
	ent= gethostbyname(arg);

    if ( hostname_lookup && ipaddr != INADDR_NONE)
	name_ent= gethostbyaddr( (char*)&ipaddr, sizeof(ipaddr), AF_INET);

    if ( ent == NULL && ipaddr != INADDR_NONE)  {
	/* Maybe gethostbyname() doesn't parse dotted IP addresses,
	 * try building a hostent by hand.
	 */
	if ( ipaddr == INADDR_NONE)  {
	     fprintf( stderr, "%s: %s\n", arg, strherror(h_errno));
	     return -1;
	}
	ent= &temp_ent;
	ent->h_name= arg;
	ent->h_aliases= NULL;
	ent->h_addrtype= 2;
	ent->h_length= 4;
	h_addr_list[0]= (char *) &ipaddr;
	h_addr_list[1]= NULL;
	ent->h_addr_list= h_addr_list;
    }
    else if ( ent == NULL)  {
        fprintf( stderr, "%s: %s\n", arg, strherror(h_errno));
        return -1;
    }

    server= (struct qserver *) malloc( sizeof( struct qserver));
    server->arg= strdup(arg);
    server->host_name= strdup((name_ent)?name_ent->h_name:ent->h_name);
    for ( a= ent->h_addr_list, i= 0; *a != NULL && i < MAXIP; a++, i++)  {
	memcpy( &server->ip[i].s_addr, *a, sizeof(server->ip[i].s_addr));
    }
    server->server_name= NULL;
    server->map_name= NULL;
    server->num_players= 0;
    server->port= port;
    server->next= servers;
    server->fd= -1;
    server->retry= n_retries;

    servers= server;
    return 0;
}


struct qserver *
find_qserver( struct sockaddr_in *addr)
{
    struct in_addr in_addr= addr->sin_addr;
    struct qserver *server= servers;
    int a;
    while ( server != NULL)  {
	for ( a= 0; a < MAXIP; a++)
	    if ( in_addr.s_addr == server->ip[a].s_addr)
		break;
	if ( a < MAXIP) break;
	server= server->next;
    }
    if ( server != NULL)
	printf( "from %s\n", server->host_name);
    return server;
}

int
bind_qserver( struct qserver *server)
{
    struct sockaddr_in addr;

    if ((server->fd= socket( AF_INET, SOCK_DGRAM, 0)) == INVALID_SOCKET) {
        perror( "socket" );
	server->server_name= SYSERROR;
        return -1;
    }

    addr.sin_family = AF_INET;
    addr.sin_port = htons(0);
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    memset( &(addr.sin_zero), 0, sizeof(addr.sin_zero) );

    if ( bind( server->fd, (struct sockaddr *)&addr,
		sizeof(struct sockaddr)) == SOCKET_ERROR) {
        perror( "bind" );
	server->server_name= SYSERROR;
        return -1;
    }

    addr.sin_family = AF_INET;
    addr.sin_port = htons((short)server->port);
    addr.sin_addr = server->ip[0];
    memset( &(addr.sin_zero), 0, sizeof(addr.sin_zero) );

    if ( connect( server->fd, (struct sockaddr *)&addr, sizeof(addr)) ==
		SOCKET_ERROR)  {
	perror( "connect");
	server->server_name= SYSERROR;
	return -1;
    }
}

void
send_packets()
{
    struct qserver *server= servers;
    while ( server != NULL)  {
	if ( server->fd != -1)  {
	    if ( ! server->retry)  {
		close( server->fd);
		server->fd= -1;
		server->server_name= TIMEOUT;
		connected--;
	    }
	    else if ( send( server->fd, (const char *)init_session,
				sizeof(init_session), 0) == SOCKET_ERROR)
		perror( "send");
	    server->retry--;
	}
	server= server->next;
    }
}

void
bind_sockets()
{
    struct qserver *server= servers;
    while ( server != NULL)  {
	if ( server->server_name == NULL && server->fd == -1 &&
		connected < MAXFD)  {
	    bind_qserver( server);
	    connected++;
	}
	server= server->next;
    }
}

void
set_fds( fd_set *fds)
{
    struct qserver *server= servers;
    while ( server != NULL)  {
	if ( server->fd != -1)
	    FD_SET( server->fd, fds);
	server= server->next;
    }
}

void
add_file( char *filename)
{
    FILE *file;
    char name[200];
    if ( strcmp( filename, "-") == 0)
	file= stdin;
    else
	file= fopen( filename, "r");

    if ( file == NULL)  {
	perror( filename);
	return;
    }
    while ( fscanf( file, "%s", name) == 1)
	add_qserver( name);

    if ( file != stdin)
	fclose(file);
}

void
usage( char **argv)
{
    printf( "Usage: %s [-H] [-u] [-nf] [-r retries] [-i interval] [-f file] host[:port] ...\n", argv[0]);
    printf( "where host is an IP address or host name\n");
    printf( "port defaults to %d if not specified\n", DEFAULT_PORT);
    printf( "-H\tresolve host names\n");
    printf( "-u\tonly display servers that are up\n");
    printf( "-nf\tonly display servers that are not full\n");
    printf( "-r\tnumber of retries, default is %d\n", DEFAULT_RETRIES);
    printf( "-i\tinterval between retries, default is %.2lf seconds\n",
	DEFAULT_RETRY_INTERVAL / 1000.0);
    printf( "-f\tread hosts from file\n");
    exit(0);
}

int
connection_refused()
{
#ifdef unix
    return errno == ECONNREFUSED;
#endif

#ifdef _WIN32
//    printf( "err = %d\n", WSAGetLastError());
    return WSAGetLastError() == WSAECONNABORTED;
#endif
}

main( int argc, char *argv[] )
{
    int pktlen, rc;
    char pkt[PACKET_LEN], *p;
    fd_set read_fds;
    fd_set error_fds;
    struct timeval timeout;
    int arg, up_servers_only= 0, no_full_servers= 0;
    struct qserver *server;
 
#ifdef _WIN32
    WORD version= MAKEWORD(1,1);
    WSADATA wsa_data;
    if ( WSAStartup(version,&wsa_data) != 0)  {
	fprintf( stderr, "Could not open winsock\n");
	exit(1);
    }
#endif

    if ( argc == 1)
	usage(argv);

    for ( arg= 1; arg < argc; arg++)  {
	if ( argv[arg][0] != '-')
	    break;
	if ( strcmp( argv[arg], "-f") == 0)  {
	    arg++;
	    if ( arg >= argc)  {
		fprintf( stderr, "missing argument for -f\n");
		usage(argv);
	    }
	    add_file( argv[arg]);
	}
	else if ( strcmp( argv[arg], "-r") == 0)  {
	    arg++;
	    if ( arg >= argc)  {
		fprintf( stderr, "missing argument for -r\n");
		usage(argv);
	    }
	    n_retries= atoi( argv[arg]);
	    if ( n_retries <= 0)  {
		fprintf( stderr, "retries must be greater than zero\n");
		exit(1);
	    }
	}
	else if ( strcmp( argv[arg], "-i") == 0)  {
	    double value= 0.0;
	    arg++;
	    if ( arg >= argc)  {
		fprintf( stderr, "missing argument for -i\n");
		usage(argv);
	    }
	    sscanf( argv[arg], "%lf", &value);
	    if ( value < 0.1)  {
		fprintf( stderr, "retry interval must be greater than 0.1\n");
		exit(1);
	    }
	    retry_interval= (int)(value * 1000);
	}
	else if ( strcmp( argv[arg], "-H") == 0)
	    hostname_lookup= 1;
	else if ( strcmp( argv[arg], "-u") == 0)
	    up_servers_only= 1;
	else if ( strcmp( argv[arg], "-nf") == 0)
	    no_full_servers= 1;
	else
	    usage(argv);
    }

    while ( arg < argc)  {
	add_qserver( argv[arg]);
	arg++;
    }
    if ( servers == NULL)
	exit(1);

    send_packets();
    bind_sockets();

    while (connected)  {
	FD_ZERO( &read_fds);
	FD_ZERO( &error_fds);
	set_fds( &read_fds);
	set_fds( &error_fds);

	timeout.tv_sec= retry_interval / 1000;
	timeout.tv_usec= (retry_interval % 1000) * 1000;

	rc= select( 64, &read_fds, NULL, &error_fds, &timeout);

	if (rc == 0)  {
	    send_packets();
	    bind_sockets();
	    continue;
	}
	if (rc == SOCKET_ERROR)  {
	    perror("select");
	    return -1;
	}

	for ( server= servers; server != NULL; server= server->next)  {
	    if ( server->fd == -1)
	        continue;
	    if ( ! FD_ISSET( server->fd, &read_fds))
		continue;
	    if ((pktlen= recv( server->fd, pkt, sizeof(pkt), 0)) ==
			SOCKET_ERROR)  {
		if ( connection_refused())  {
		    if ( ! up_servers_only)
			printf( "%-16s %10s\n", server->arg, DOWN);
		    server->server_name= DOWN;
		    close(server->fd);
		    server->fd= -1;
		    connected--;
		}
		continue;
	    }
	    if ( server->server_name != NULL)
		continue;

	    p= & pkt[5];
	    server->address= strdup(p);
	    p+= strlen(p) + 1;
	    server->server_name= strdup(p);
	    p+= strlen(p) + 1;
	    server->map_name= strdup(p);
	    server->max_players= pkt[pktlen-2];
	    server->num_players= pkt[pktlen-3];

	    if ( ! no_full_servers || server->num_players < server->max_players)
		display_stats( server);

	    close(server->fd);
	    server->fd= -1;
	    connected--;
	}
    }

    server= servers;
    while ( server != NULL)  {
	if ( ! up_servers_only && server->server_name == TIMEOUT)
	    printf( "%-16s no response\n",
		(hostname_lookup) ? server->host_name : server->arg);
	server= server->next;
    }
    return 0;
}

int
print_packet( char *buf, int buflen)
{
    unsigned int *intbuf= (unsigned int *) buf;
    int i;
    for ( i= 0; i < buflen; i++)  {
	if ( isprint( buf[i])) printf( "%c", buf[i]);
	else printf( " %02x ", (unsigned int)buf[i]);
    }
    puts("");
}

