#!/usr/bin/perl
# $Id$

# Copyright 2000 by Mark Wedel.
# This script follows the same license as crossfire (GPL).

use Socket;
use English;

# We periodically generate a nice HTML file that people can't put their
# web browser at.  This is the location of that file.
$HTML_FILE="/var/apache/htdocs/metaserver.html";

# Cache file to keep data we ahve collected.  This is used so that if
# the metaserver program crashes/dies, it still has some old data.
# You may want to set this to a location that will survive across system
# reboots.
$CACHE_FILE="/var/tmp/meta_xfire.cache";

# We remove a server after $REMOVE_SERVER number of seconds of no updates.
# 600 is 10 minutes - if we haven't gotten an update that fast, the server
# is almost certainly gone/not available.  This reduces congestion when
# someone on a dhcp connection keeps running a server and it fills
# up a bunch of the slots.
$REMOVE_SERVER=600;

# UPDATE_SYNC determines how often we update the HTML_FILE and CACHE_FILE.
$UPDATE_SYNC=300;

# IP_INTERVAL is how often (in seconds) same IP can request metaserver
# info.  This is to prevent DOS attacks.

$IP_INTERVAL=5;

# For gathering some anonymous statistics. You probably want to use
# MRTG/RRDTOOL for generating statistics from the file.
# -- Heikki Hokkanen, 2005-03-26
my $STATS_FILE="/var/tmp/meta_xfire.stats";
my $stats_updatehits = 0; # COUNTER
my $stats_requesthits = 0; # COUNTER
my $stats_totalplayers = 0; # GAUGE
my $stats_totalservers = 0; # GAUGE

socket(SOCKET, PF_INET, SOCK_DGRAM, getprotobyname("udp")) ||
	die("$0: can not open udp socket: $OS_ERROR\n");
bind(SOCKET, sockaddr_in(13326, INADDR_ANY)) ||
	die("$0: Can not bind to socket: $OS_ERROR\n");

# Socket1 is used for incoming requests - if we get a connection on this,
# we dump out our data to that socket in an easily parsible form.
socket(SOCKET1, PF_INET, SOCK_STREAM, getprotobyname("tcp")) ||
	die("$0: can not open tcp socket: $OS_ERROR\n");

# errors on this not that critical
setsockopt(SOCKET1, SOL_SOCKET, SO_REUSEADDR, 1);

bind(SOCKET1, sockaddr_in(13326, INADDR_ANY)) ||
	die("$0: Can not bind to socket: $OS_ERROR\n");
listen(SOCKET1, 10) ||
	die("$0: Can not listen on socket: $OS_ERROR\n");

vec($rin, fileno(SOCKET), 1)=1;
vec($rin, fileno(SOCKET1), 1)=1;

if (open(CACHE,"<$CACHE_FILE")) {
    while (<CACHE>) {
	chomp;
	($ip, $rest) = split /\|/, $_, 2;
	$data{$ip} = $_;
    }
}
close(CACHE);

$last_sync=time;

while (1) {
    $nfound=select($rout=$rin, undef, undef, 60);
    $cur_time=time;
    if ($nfound) {
	if (vec($rout, fileno(SOCKET),1)) {
	    $ipaddr = recv(SOCKET, $data, 256, 0) ||
		print STDERR "$0: error on recv call: $OS_ERROR\n";
	    ($port, $ip) = sockaddr_in($ipaddr);
	    $host = inet_ntoa $ip;
	    ($name, $rest) = split /\|/, $data;
	    if ($name ne "put.your.hostname.here") {
		$data{$host} = "$host|$cur_time|$data";
		$stats_updatehits++;
	    }
	}
	if (vec($rout, fileno(SOCKET1),1)) {
	    # This is overly basic - if there are enough servers
	    # where the output won't fit in the outgoing socket,
	    # this will block.  However, if we fork, we open
	    # ourselves up to someone intentionally designing something
	    # that causes these to block, and then have us fork a huge
	    # number of process potentially filling up our proc table.
	    if ($ipaddr=accept NEWSOCKET, SOCKET1) {
		($port, $ip ) = sockaddr_in( $ipaddr );
		$dq = join('.',unpack('C4', $ip));
		if ($ip_times{$dq} > time) {
		    close(NEWSOCKET);
 		} else {
		    $ip_times{$dq} = time + $IP_INTERVAL;
		    foreach $i (keys %data) {
		        # Report idle time to client, and not last update
		    	# as we received in seconds since epoch.
			($ip, $time, $rest) = split /\|/, $data{$i}, 3;
		    	$newtime = $cur_time - $time;
		    	print NEWSOCKET "$ip|$newtime|$rest\n";
		    }
		    close(NEWSOCKET);
		    $stats_requesthits++;
		}
	    }
	}
    }

    # Need to generate some files.  This is also where we remove outdated
    # hosts.
    if ($last_sync+$UPDATE_SYNC < $cur_time) {
	$last_sync = $cur_time;
	open(CACHE,">$CACHE_FILE");
	open(HTML,">$HTML_FILE");

	print HTML
'<title>Crossfire Server List</title>
<h1 align=center>Crossfire Server List</h1><p>
<table border=1 align=center cellpadding=5>
<tr>
<th>IP Address</th><th>Last Update Date/Time</th><th>Last Update Minutes Elapsed</th>
<th>Hostname</th><th>Number of Players</th><th>Version</th><th>Comment</th>
</tr>
';

	$stats_totalplayers = 0;
	$stats_totalservers = 0;
	foreach $i (keys %data) {
		$stats_totalservers++;
	    ($ip, $time, @rest) = split /\|/, $data{$i};
	    if ($time+$REMOVE_SERVER<$cur_time) {
		delete $data{$i};
	    } else {
		print CACHE "$data{$i}\n";
		$elapsed = int(($cur_time - $time)/60);
		$gmtime = gmtime($time);
		print HTML "<tr><td>$i</td><td>$gmtime</td><td>$elapsed</td>";
		print HTML "<td>$rest[0]</td><td>$rest[1]</td><td>$rest[2]</td><td>$rest[3]</td></tr>\n";
		$stats_totalplayers += int($rest[2]);
	    }
	}
	$gmtime = gmtime($cur_time);
	print HTML "
</table><p>
The server name is reported by the server, while the ip address is determined by
the incoming data packet.  These values may not resolve to the same thing in the
case of multi homed hosts or multi ip hosts.<p>

All times are in GMT.<p>

<font size=-2>Last Updated: $gmtime<font size=+2><p>";
	close(HTML);
	close(CACHE);

	open(STATS,">$STATS_FILE");
	print STATS "$stats_updatehits:$stats_requesthits:$stats_totalservers:$stats_totalplayers\n";
	close(STATS);

    }
}
