aboutsummaryrefslogtreecommitdiff
path: root/lib/MasterServer/UDP
diff options
context:
space:
mode:
Diffstat (limited to 'lib/MasterServer/UDP')
-rwxr-xr-xlib/MasterServer/UDP/BeaconCatcher.pm15
-rwxr-xr-xlib/MasterServer/UDP/BeaconChecker.pm147
-rwxr-xr-xlib/MasterServer/UDP/DatagramProcessor.pm245
-rwxr-xr-xlib/MasterServer/UDP/UCCAppletQuery.pm86
-rwxr-xr-xlib/MasterServer/UDP/UDPTicker.pm298
-rwxr-xr-xlib/MasterServer/UDP/UpLink.pm169
6 files changed, 705 insertions, 255 deletions
diff --git a/lib/MasterServer/UDP/BeaconCatcher.pm b/lib/MasterServer/UDP/BeaconCatcher.pm
index 0d5ce81..06fd38a 100755
--- a/lib/MasterServer/UDP/BeaconCatcher.pm
+++ b/lib/MasterServer/UDP/BeaconCatcher.pm
@@ -16,8 +16,8 @@ our @EXPORT = qw| beacon_catcher on_beacon_receive|;
sub beacon_catcher {
my $self = shift;
- # module startup log
- $self->log("load","Loading UDP Beacon Catcher.");
+ # display that the server is up and listening for beacons
+ $self->log("info", "Listening for UDP beacons on port $self->{beacon_port}.");
# UDP server
my $udp_server;
@@ -30,9 +30,6 @@ sub beacon_catcher {
on_recv => sub {$self->on_beacon_receive(@_)},
);
- # display that the server is up and listening for beacons
- $self->log("info", "Listening for UT Beacons on port $self->{beacon_port}.");
-
# allow object to exist beyond this scope. Objects have ambitions too.
return $udp_server;
}
@@ -57,14 +54,18 @@ sub on_beacon_receive {
# truncate and try to continue
$b = substr $b, 0, 64;
}
-
+
# if a heartbeat format was detected...
$self->process_udp_beacon($udp, $pa, $b, $peer_addr, $port)
if ($b =~ m/\\heartbeat\\/ && $b =~ m/\\gamename\\/);
-
+
# or if this is a secure response, verify the response
$self->process_udp_validate($b, $peer_addr, $port, undef)
if ($b =~ m/\\validate\\/);
+
+ # or if other masterservers check if we're still alive
+ $self->process_udp_basic($udp, $pa, $b, $peer_addr)
+ if ($b =~ m/\\basic\\/ || $b =~ m/\\status\\/ || $b =~ m/\\info\\/);
}
1;
diff --git a/lib/MasterServer/UDP/BeaconChecker.pm b/lib/MasterServer/UDP/BeaconChecker.pm
index a99be72..f74378d 100755
--- a/lib/MasterServer/UDP/BeaconChecker.pm
+++ b/lib/MasterServer/UDP/BeaconChecker.pm
@@ -6,93 +6,7 @@ use warnings;
use AnyEvent::Handle::UDP;
use Exporter 'import';
-our @EXPORT = qw| beacon_checker query_udp_server|;
-
-################################################################################
-## When addresses are stored in the 'pending' list, they are supposed to be
-## queried immediately with the secure/validate challenge to testify that
-## the server is genuine and alive.
-##
-## Some servers do not support the secure-challenge on the Uplink port. These
-## servers are verified with a secure-challenge on their heartbeat ports,
-## which are designed to respond to secure queries, as well as status queries.
-##
-## Addresses collected by other scripts, whether from the UCC applet or manual
-## input via the website, are added to the pending list. It is more
-## important to verify pending beacons and new server addresses, than to
-## update the status of existing addresses. Therefore, pending addresses are
-## prioritized.
-################################################################################
-sub beacon_checker {
- my $self = shift;
- $self->log("load", "UDP Beacon Checker is loaded.");
-
- # queue -- which address is next in line?
- my %q = ( pending_id => 0, server_id => 0,
- start_time => time+$self->{beacon_checker_time}[0]-1); #time+grace
-
- # go through all servers one by one, new and old
- my $server_info = AnyEvent->timer (
- after => $self->{beacon_checker_time}[0],
- interval => $self->{beacon_checker_time}[1],
- cb => sub {
-
- # first of all, check whether we exceeded our time cap limit
- if ( (time - $q{start_time}) >= $self->{beacon_checker_time}[2] ){
- # reset queue variables
- $q{pending_id} = 0;
- $q{server_id} = 0;
- $q{start_time} = time;
- }
-
- # See if there are pending servers, and use existing secure string for
- # the challenge.
- my $n = $self->get_next_pending($q{pending_id});
-
- # if any entries were found, proceed
- if ( $n->[0] ) {
-
- # next pending id will be > $n
- $q{pending_id} = $n->[0];
-
- # query the server
- $self->query_udp_server($n->[1], $n->[2], $n->[3]);
-
- # work done. Wait for the next round for the next timer tick.
- return;
- }
-
- # if no pending servers left, update the other entries
- $n = $self->get_next_server($q{server_id});
-
- # if any entries were found, proceed
- if ( $n->[0] ) {
-
- # next server id will be > $n
- $q{server_id} = $n->[0];
-
- # query the server (no secure string)
- $self->query_udp_server($n->[1], $n->[2], "");
-
- # work done. Wait for the next round for the next task.
- return;
- }
-
- # At this point, we are out of server entries. When new servers are
- # added, they are immediately queried on the next round.
- # From here on, just count down until the cycle is complete and handle
- # new entries while they are added to the list.
-
- }
- );
-
- # at the start of the module, remind host how often this happens
- $self->log("info", "Verifying servers every $self->{beacon_checker_time}[2] seconds.");
-
- # return the timer object to keep it alive outside of this scope
- return $server_info;
-}
-
+our @EXPORT = qw| query_udp_server|;
################################################################################
## Get the server status from any server over UDP and store the received
@@ -100,11 +14,11 @@ sub beacon_checker {
## secure/pending or information.
################################################################################
sub query_udp_server {
- my ($self, $ip, $port, $secure) = @_;
+ my ($self, $id, $ip, $port, $secure, $message_type) = @_;
my $buf = "";
# debug spamming
- $self->log("udp", "Query server $ip:$port");
+ $self->log("udp", "Query server $id ($ip:$port)");
# connect with UDP server
my $udp_client; $udp_client = AnyEvent::Handle::UDP->new(
@@ -118,40 +32,51 @@ sub query_udp_server {
# add packet to buffer
$buf .= $_[0];
+ # message type 0: \basic\\info\
+ # if gamename, ver, hostname and hostport are available, but NOT the value
+ # "listenserver", it would have been \basic\info
+ if ($buf =~ m/\\gamename\\/ &&
+ $buf =~ m/\\hostname\\/ &&
+ $buf =~ m/\\hostport\\/ &&
+ $buf !~ m/\\listenserver\\/ ) {
+ $self->process_query_response($buf, $ip, $port);
+ }
+
+ # message type 1: \basic\\secure\wookie
# if validate, assume that we sent a \basic\secure request.
if ($buf =~ m/\\validate\\/){
$self->process_udp_validate($buf, $ip, undef, $port);
}
- # if gamename, ver, hostname and hostport are available, it should
- # have been \basic\info
- elsif ($buf =~ m/\\gamename\\/ && $buf =~ m/\\gamever\\/
- && $buf =~ m/\\hostname\\/ && $buf =~ m/\\hostport\\/) {
- $self->process_query_response($buf, $ip, $port);
+
+ # message type 2: \status\
+ # contains same info as \basic\\info, but also "listenserver". Only for UT.
+ if ($buf =~ m/\\gamename\\ut/ &&
+ $buf =~ m/\\hostname\\/ &&
+ $buf =~ m/\\hostport\\/ &&
+ $buf =~ m/\\listenserver\\/ ) {
+ $self->process_status_response($buf, $ip, $port);
}
+
# else partial information received. wait for more.
- else{ }
+ # else { }
},
);
#
# Send secure message or status, depending on provided variables
- #
+ # Message types can be
+ # 0: \basic\\info\
+ # 1: \basic\\secure\wookie
+ # 2: \status\
+ #
- # secure servers enabled and secure key provided
- if ($secure ne "" && $self->{require_secure_beacons} > 0) {
- # send secure
- $udp_client->push_send("\\basic\\\\secure\\$secure");
-
- # and log that we sent it
- $self->log("udp", "sending secure=\"$secure\" to $ip:$port");
- }
- else {
- # send information request
- $udp_client->push_send("\\basic\\\\info\\");
-
- # and log that we sent it
- $self->log("udp","sending basic request to $ip:$port");
- }
+ # determine the message
+ my $message = "\\basic\\\\info\\"; # default 0
+ $message = "\\basic\\\\secure\\$secure" if ($secure ne "" && $self->{require_secure_beacons} > 0); # message_type 1
+ $message = "\\status\\" if ($message_type == 2);
+
+ # send selected message
+ $udp_client->push_send($message);
}
1;
diff --git a/lib/MasterServer/UDP/DatagramProcessor.pm b/lib/MasterServer/UDP/DatagramProcessor.pm
index f80f6fb..863512c 100755
--- a/lib/MasterServer/UDP/DatagramProcessor.pm
+++ b/lib/MasterServer/UDP/DatagramProcessor.pm
@@ -1,4 +1,3 @@
-
package MasterServer::UDP::DatagramProcessor;
use strict;
@@ -10,6 +9,7 @@ use Exporter 'import';
our @EXPORT = qw| process_udp_beacon
process_udp_validate
process_query_response
+ process_status_response
process_ucc_applet_query |;
################################################################################
@@ -23,8 +23,10 @@ sub process_udp_beacon {
# received heartbeat in $buf: \heartbeat\7778\gamename\ut\
my %r;
+ my $raw = $buf; # raw buffer for logging if necessary
$buf = encode('UTF-8', $buf);
$buf =~ s/\\([^\\]+)\\([^\\]+)/$r{$1}=$2/eg;
+
# check whether the beacon has a gamename
if (defined $r{gamename}) {
@@ -32,19 +34,37 @@ sub process_udp_beacon {
$self->log("beacon", "$peer_addr:$r{heartbeat} for $r{gamename}");
# some games (like bcommander) have a default port and don't send a heartbeat port.
- $r{heartbeat} = $self->get_default_port($r{gamename}) if ($r{heartbeat} == 0);
+ $r{heartbeat} = $self->get_game_props($r{gamename})->{heartbeat} if ($r{heartbeat} == 0);
#
# verify valid server address (ip+port)
if ($self->valid_address($peer_addr,$r{heartbeat})) {
+
+ # check if the entry already was not added within the last 5 seconds, throttle otherwise
+ my $throttle = $self->get_pending(
+ ip => $peer_addr,
+ heartbeat => $r{heartbeat},
+ gamename => $r{gamename},
+ after => 5,
+ sort => "added",
+ limit => 1
+ )->[0];
+ return if (defined $throttle);
# generate a new secure string
my $secure = $self->secure_string();
# update beacon in serverlist if it already exists, otherwise update
# or add to pending with new secure string.
- my $auth = $self->add_beacon($peer_addr, $port, $r{heartbeat}, $r{gamename}, $secure);
-
+ my $auth = $self->add_server_new(ip => $peer_addr,
+ beaconport => $port,
+ heartbeat => $r{heartbeat},
+ gamename => $r{gamename},
+ secure => $secure,
+ direct => 1,
+ updated => time,
+ beacon => time);
+
# send secure string back
if ($auth > 0) {
@@ -59,14 +79,21 @@ sub process_udp_beacon {
# invalid ip+port combination, like \heartbeat\0\ or local IP
else {
# Log that beacon had incorrect information, such as port 0 or so. Spams log!
- $self->log("invalid","$peer_addr:$r{heartbeat} ($r{heartbeat}) had bad information");
+ $self->log("invalid","$peer_addr had bad information --> $raw");
}
}
- # gamename not valid or not found in supportedgames.pl
+ # gamename not valid or recognized, display raw buffer in case data could not
+ # be extrapolated from the heartbeat
else {
# log
- $self->log("support", "received unknown beacon \"$r{gamename}\" from $peer_addr:$r{heartbeat}");
+ $self->log("support", "received unknown beacon from $peer_addr --> $raw");
+ #
+ # TODO: more practical way to log this to the database: new table
+ # named "unsupported" where messages are logged by ip, port, gamename (if
+ # applicable) and TEXT raw message.
+ #
+
}
}
@@ -78,51 +105,83 @@ sub process_udp_validate {
# $self, udp data, ip, port
my ($self, $buf, $peer_addr, $port, $heartbeat) = @_;
+ # debug spamming
+ # $self->log("udp", "Received response from $peer_addr:$heartbeat, sent |$buf|");
+
# received heartbeat in $b: \validate\string\queryid\99.9\
my %r;
$buf = encode('UTF-8', $buf);
$buf =~ s/\\([^\\]+)\\([^\\]+)/$r{$1}=$2/eg;
# get our existing knowledge about this server from the database
- # if the heartbeat/queryport known? then use that instead as beacon ports --> may vary after server restarts!
- my $pending = (defined $heartbeat)
- ? $self->get_pending_info($peer_addr, $heartbeat)
- : $self->get_pending_beacon($peer_addr, $port);
+ my $pending = $self->get_pending(
+ ip => $peer_addr,
+ limit => 1,
+ ($heartbeat ? (heartbeat => $heartbeat) : () ),
+ ($port ? (beaconport => $port) : () ),
+ )->[0];
- # if indeed in the pending list, check -- if this entry is not (longer) in the list, it
+ # if indeed in the pending list, check; -- if this entry is not (longer) in the list, it
# was either removed by the BeaconChecker or cleaned out in maintenance (after X hours).
if (defined $pending) {
#determine if it uses any enctype
my $enc = (defined $r{enctype}) ? $r{enctype} : 0;
- # database may not contain the correct gamename (ucc applet, incomplete beacon, change of gameserver)
- $pending->[4] = (defined $r{gamename}) ? $r{gamename} : $pending->[4];
+ # database may not contain the correct gamename (ucc applet, incomplete beacon, other game)
+ $pending->{gamename} = $r{gamename} if (defined $r{gamename});
- # verify challenge gamename secure enctype validate_response
- my $val = $self->validated_beacon($pending->[4], $pending->[5], $enc, $r{validate});
-
- # log challenge results ($port may not have been provided)
- $port = (defined $port) ? $port : $heartbeat;
- $self->log("secure", "$peer_addr:$port validated with $val for $pending->[4]");
+ # verify challenge
+ my $val = $self->compare_challenge(
+ gamename => $pending->{gamename},
+ secure => $pending->{secure},
+ enctype => $r{enctype},
+ validate => $r{validate},
+ ignore => $self->{ignore_beacon_key},
+ );
- # if validated, add to db
- if ($val > 0) {
+ # if validated, add server to database
+ if ($val > 0 || $self->{require_secure_beacons} == 0) {
- # successfully added? ip, query port, gamename
- my $sa = $self->add_to_serverlist($pending->[1], $pending->[3], $pending->[4]);
+ # select server from serverlist -- should not exist yet.
+ my $srv = $self->get_server(ip => $pending->{ip}, port => $pending->{heartbeat})->[0];
- # remove the entry from pending if successfully added
- $self->remove_pending($pending->[0]) if ( $sa >= 0);
+ # was found, then update gamename and remove from pending
+ if (defined $srv) {
+ my $sa = $self->update_server_list(
+ ip => $pending->{ip},
+ port => $pending->{heartbeat},
+ gamename => $pending->{gamename}
+ );
+ # remove the entry from pending if successfully added
+ $self->remove_pending($pending->{id}) if ( $sa >= 0);
+ }
+ # was not found, insert clean and remove from pending
+ else {
+ my $sa = $self->add_server_list(
+ ip => $pending->{ip},
+ port => $pending->{heartbeat},
+ gamename => $pending->{gamename}
+ );
+ # remove the entry from pending if successfully added
+ $self->remove_pending($pending->{id}) if ( $sa > 0);
+ }
}
else {
- # else failed validation
- $self->log("error","beacon $peer_addr:$port failed validation for $pending->[4] (details: $pending->[5] sent, got $r{validate})");
+ # else failed validation
+ # calculate expected result for log
+ my $validate_string = $self->validate_string(
+ gamename => $pending->{gamename},
+ secure => $pending->{secure}
+ );
+ $self->log("secure","$pending->{id} for $pending->{gamename} sent: $pending->{secure}, got $r{validate}, expected $validate_string");
}
}
+ # if no entry found in pending list
else {
- # else failed validation
- $self->log("error","server not found in pending for $peer_addr and unknown heartbeat/port");
+ # 404 not found
+ $self->log("error","server not found in pending for $peer_addr:",
+ ($heartbeat ? $heartbeat : "" ), ($port ? $port : "" ), " !");
}
}
@@ -140,37 +199,116 @@ sub process_query_response {
$buf =~ s/\\([^\\]+)\\([^\\]+)/$s{$1}=$2/eg;
# check whether the gamename is supported in our db
- if (defined $s{gamename} && length $self->get_cipher(lc $s{gamename}) > 1) {
+ if (defined $s{gamename} &&
+ length $self->get_game_props($s{gamename})->{cipher} > 1) {
# parse variables
my %nfo = ();
-
- $nfo{gamename} = lc $s{gamename};
$nfo{gamever} = exists $s{gamever} ? $s{gamever} : "";
$nfo{hostname} = exists $s{hostname} ? $s{hostname} : "$ip:$port";
$nfo{hostport} = exists $s{hostport} ? $s{hostport} : 0;
- # some mor0ns have values longer than 100 characters
- $nfo{hostname} = substr $nfo{hostname}, 0, 99 if (length $nfo{hostname} >= 99);
+ # some mor0ns have hostnames longer than 200 characters
+ $nfo{hostname} = substr $nfo{hostname}, 0, 199 if (length $nfo{hostname} >= 199);
# log results
- $self->log("hostname", "$ip:$port\t is now known as\t $nfo{hostname}");
+ $self->log("hostname", "$ip:$port is now known as $nfo{hostname}");
- # if only validated servers are allowed in the list
- if ($self->{require_secure_beacons} > 0) {
- # only update in database
- $self->update_serverlist($ip, $port, \%nfo);
- }
- # otherwise also add the server to serverlist if required
- else{
- # add to serverlist and update anyway
- $self->add_to_serverlist($ip, $port, $nfo{gamename});
- $self->update_serverlist($ip, $port, \%nfo);
+ # add or update in serverlist (assuming validation is complete)
+ $self->update_server_list(
+ ip => $ip,
+ port => $port,
+ gamename => $s{gamename},
+ %nfo);
+
+ # if address is in pending list, remove it
+ my $pen = $self->get_pending(ip => $ip, heartbeat => $port)->[0];
+ $self->remove_pending($pen->{id}) if $pen;
+ }
+}
+
+################################################################################
+## Process status data that was obtained with \status\ from the
+## UT serverstats checker module.
+################################################################################
+sub process_status_response {
+ # $self, udp data, ip, port
+ my ($self, $buf, $ip, $port) = @_;
+
+ #process datastream
+ my %s;
+ $buf = encode('UTF-8', $buf);
+ $buf =~ s/\\([^\\]+)\\([^\\]+)/$s{$1}=$2/eg;
+
+ # check whether this server is in our database
+ my $serverlist_id = $self->get_server(ip => $ip, port => $port)->[0];
+
+ # only allow servers that were approved/past pending
+ if (defined $serverlist_id) {
+
+ #
+ # pre-process variables before putting them in the db
+ #
+
+ # gamename should in all cases be "ut" (we only allow this for UT games at the moment!)
+ return if (!defined $s{gamename} || $s{gamename} ne "ut");
+
+ # some people trying to sneak their Unreal servers into the UT serverlist
+ return if (!defined $s{gamever} || $s{gamever} eq "227i");
+
+ # some sanity checks for the presentation
+ $s{hostname} = substr $s{hostname}, 0, 199 if ($s{hostname} && length $s{hostname} >= 199);
+ $s{mapname} = substr $s{mapname}, 0, 99 if ($s{mapname} && length $s{mapname} >= 99);
+ $s{maptitle} = substr $s{maptitle}, 0, 99 if ($s{maptitle} && length $s{maptitle} >= 99);
+
+ #
+ # Store info in database
+ #
+
+ # check if the ID already exists in the database
+ my $utserver_id = $self->get_utserver(id => $serverlist_id->{id})->[0];
+
+ # add and/or update
+ $self->add_utserver($ip, $port) if (not defined $utserver_id);
+ $self->update_utserver($serverlist_id->{id}, %s);
+
+ #
+ # Player info
+ #
+
+ # delete all players for this server.
+ $self->delete_utplayers($serverlist_id->{id});
+
+ # iterate through all player IDs and add them to the database
+ for (my $i = 0; exists $s{"player_$i"}; $i++) {
- # if address is in pending list, remove it
- my $pending = $self->get_pending_info($ip, $port);
- $self->remove_pending($pending->[0]) if $pending;
+ # shorten name (some people might be overcompensating their names)
+ $s{"player_$i"} = substr $s{"player_$i"}, 0, 39 if (length $s{"player_$i"} > 39);
+
+ my %player = ();
+ $player{player} = exists $s{"player_$i"} ? $s{"player_$i"} : "Player";
+ $player{team} = exists $s{"team_$i"} ? $s{"team_$i"} : 255;
+ $player{team} = ($player{team} =~ m/^[0-3]/ ) ? int($player{team}) : 255;
+ $player{frags} = exists $s{"frags_$i"} ? int($s{"frags_$i"}) : 0;
+ $player{mesh} = exists $s{"mesh_$i"} ? $s{"mesh_$i"} : "";
+ $player{skin} = exists $s{"skin_$i"} ? $s{"skin_$i"} : "";
+ $player{face} = exists $s{"face_$i"} ? $s{"face_$i"} : "";
+ $player{ping} = exists $s{"ping_$i"} ? int($s{"ping_$i"}) : 0;
+ $player{ngsecret} = exists $s{"ngsecret_$i"} ? $s{"ngsecret_$i"} : ""; # contains bot info
+
+ # write to db
+ $self->insert_utplayer($serverlist_id->{id}, %player);
}
+
+ #
+ # Prevent null concatenation in logging
+ $s{numplayers} ||= 0;
+ $s{maxplayers} ||= 0;
+ $s{mapname} ||= "Unknown map";
+ $s{hostname} ||= "Unknown hostname";
+
+ # log results
+ $self->log("utserver", "$serverlist_id->{id}, $ip:$port,\t $s{numplayers}/$s{maxplayers} players, $s{mapname}, $s{hostname}");
}
}
@@ -204,7 +342,12 @@ sub process_ucc_applet_query {
$self->log("add", "applet query added $ms->{game}\t$a\t$p");
# add server
- $self->add_pending($a, $p, $ms->{game}, $self->secure_string());
+ $self->add_server_new(ip => $a,
+ beaconport => $p,
+ heartbeat => $p,
+ gamename => $ms->{game},
+ secure => $self->secure_string(),
+ updated => time);
}
# invalid address, log
else {$self->log("error", "invalid address found at master applet $ms->{ip}: $l!");}
@@ -215,7 +358,7 @@ sub process_ucc_applet_query {
$self->{dbh}->commit;
# print findings
- $self->log("applet","found $c addresses at $ms->{ip} for $ms->{game}.");
+ $self->log("applet-rx","found $c addresses at $ms->{ip} for $ms->{game}.");
}
diff --git a/lib/MasterServer/UDP/UCCAppletQuery.pm b/lib/MasterServer/UDP/UCCAppletQuery.pm
deleted file mode 100755
index f92c20c..0000000
--- a/lib/MasterServer/UDP/UCCAppletQuery.pm
+++ /dev/null
@@ -1,86 +0,0 @@
-
-package MasterServer::UDP::UCCAppletQuery;
-
-use strict;
-use warnings;
-use AnyEvent;
-use AnyEvent::Handle;
-use Exporter 'import';
-
-our @EXPORT = qw| ucc_applet_query_scheduler query_applet |;
-
-################################################################################
-## Query Epic Games'-based UCC applets periodically to get an additional
-## list of online UT, Unreal (or other) game servers.
-################################################################################
-sub ucc_applet_query_scheduler {
- my $self = shift;
- $self->log("load", "UCC Applet Query Scheduler is loaded.");
-
- my $i = 0;
- return AnyEvent->timer (
- after => $self->{master_applet_time}[0],
- interval => $self->{master_applet_time}[1],
- cb => sub {
- # check if there's a master server entry to be queried. If not, return
- # to zero and go all over again.
- $i = 0 unless $self->{master_applet}[$i];
- return if (!defined $self->{master_applet}[$i]);
-
- # perform the query
- $self->query_applet($self->{master_applet}[$i]);
-
- #increment counter
- $i++;
- }
- );
-}
-
-################################################################################
-## The UCC Applet (Epic Megagames, Inc.) functions as a master server for one
-## single game. However, it does not always follow the defined protocol.
-## This module connects with UCC masterserver applets to receive the list.
-################################################################################
-sub query_applet {
- my ($self, $ms) = @_;
-
- # be nice to notify
- $self->log("query","start querying $ms->{ip}:$ms->{port} for '$ms->{game}' games");
-
- # list to store all IPs in.
- my $master_list = "";
-
- # connection handle
- my $handle;
- $handle = new AnyEvent::Handle(
- connect => [$ms->{ip} => $ms->{port}],
- timeout => 5,
- poll => 'r',
- on_error => sub {$self->log("error", "$! on $ms->{ip}:$ms->{port}."); $handle->destroy;},
- on_eof => sub {$self->process_ucc_applet_query($master_list, $ms); $handle->destroy;},
- on_read => sub {
-
- # receive and clear buffer
- my $m = $_[0]->rbuf;
- $_[0]->rbuf = "";
-
- # remove string terminator
- chop $m if $m =~ m/secure/;
-
- # part 1: receive \basic\\secure\$key
- if ($m =~ m/\\basic\\\\secure\\/) {
- # skip to part 3: also request the list \list\gamename\ut -- skipped in UCC applets
- #$handle->push_write("\\list\\\\gamename\\$ms->{game}");
- $handle->push_write("\\list\\");
- }
-
- # part 3b: receive the entire list in multiple steps.
- if ($m =~ m/\\ip\\/) {
- # add buffer to the list
- $master_list .= $m;
- }
- }
- );
-}
-
-1;
diff --git a/lib/MasterServer/UDP/UDPTicker.pm b/lib/MasterServer/UDP/UDPTicker.pm
new file mode 100755
index 0000000..5a34a8f
--- /dev/null
+++ b/lib/MasterServer/UDP/UDPTicker.pm
@@ -0,0 +1,298 @@
+
+package MasterServer::UDP::UDPTicker;
+
+use strict;
+use warnings;
+use AnyEvent::Handle::UDP;
+use Exporter 'import';
+
+our @EXPORT = qw| udp_ticker |;
+
+################################################################################
+## When addresses are stored in the 'pending' list, they are supposed to be
+## queried immediately with the secure/validate challenge to testify that
+## the server is genuine and alive.
+##
+## Some servers do not support the secure-challenge on the Uplink port. These
+## servers are verified with a secure-challenge on their heartbeat ports,
+## which are designed to respond to secure queries, as well as status queries.
+##
+## Addresses collected by other scripts, whether from the UCC applet or manual
+## input via the website, are added to the pending list. It is more
+## important to verify pending beacons and new server addresses, than to
+## update the status of existing addresses. Therefore, pending addresses are
+## prioritized.
+##
+## Another function required for 333networks is the "server info" part of the
+## site. UT servers are queried and stored in the database. This is the lowest
+## priority for the masterserver and is therefore performed last.
+##
+################################################################################
+sub udp_ticker {
+ my $self = shift;
+
+ # inform that we are running
+ $self->log("info", "UDP Ticker is loaded.");
+
+ # queue -- which address is next in line?
+ my %reset = (start => time, id => 0);
+ my %pending = (%reset, c => 0, limit => 900); # 900s ~ 15m
+ my %updater = (%reset, c => 0, limit => 1800); # 1800s ~ 30m
+ my %ut_serv = (%reset, c => 0, limit => 300); # 300s ~ 5m
+ my %oldserv = (%reset, c => 0, limit => 86400); # 86400s ~ 24h
+
+ my $debug_counter = 0;
+
+ # go through all servers that need querying
+ my $server_info = AnyEvent->timer (
+ after => 75, # first give beacons a chance to uplink
+ interval => 0.2, # 5 addresses per second is fast enough
+ cb => sub {
+
+ # after the first full run was completed, reset the counters when loop time expires
+ if (defined $self->{firstrun}) {
+ # reset timer
+ %reset = (start => time, id => 0, c => 0);
+
+ #
+ # it can happen that a run takes more than the allowed time
+ # in that case, allow more time
+ #
+
+ # pending
+ if (time - $pending{start} > $pending{limit}) {
+ if ($pending{c} > 0) {
+ # done within defined time, reset
+ %pending = (%pending, %reset);
+ }
+ }
+
+ # ut servers
+ if (time - $ut_serv{start} > $ut_serv{limit}) {
+ if ($ut_serv{c} > 0) {
+ # done within defined time, reset
+ %ut_serv = (%ut_serv, %reset)
+ }
+ }
+
+ # updater
+ if (time - $updater{start} > $updater{limit}) {
+ if ($updater{c} > 0) {
+ # done within defined time, reset
+ %updater = (%updater, %reset);
+ }
+ }
+
+ # old servers
+ if (time - $oldserv{start} > $oldserv{limit}) {
+ if ($oldserv{c} > 0) {
+ %oldserv = (%oldserv, %reset);
+ }
+ }
+
+ #
+ # else { print "Making overtime!" }
+
+=pod
+ # FIXME remove this if above works
+
+ # debug: detect premature resets
+ if (time - $pending{start} > $pending{limit}) {
+ if ($pending{c} == 0) {
+ print "Premature pending reset\n" ;
+ }
+ else{$pending{c} = 0;}
+ }
+
+ if (time - $updater{start} > $updater{limit}) {
+ if ($updater{c} == 0) {
+ print "Premature updater reset\n" ;
+ }
+ else{$updater{c} = 0;}
+ }
+
+ if (time - $ut_serv{start} > $ut_serv{limit}) {
+ if ($ut_serv{c} == 0) {
+ print "Premature ut_serv reset\n" ;
+ }
+ else{$ut_serv{c} = 0;}
+ }
+
+ if (time - $oldserv{start} > $oldserv{limit}) {
+ if ($oldserv{c} == 0) {
+ print "Premature oldserv reset\n" ;
+ }
+ else{$oldserv{c} = 0;}
+ }
+
+ # are we making overtime on any of the counters yet?
+ %pending = (%pending, %reset) if (time - $pending{start} > $pending{limit});
+ %updater = (%updater, %reset) if (time - $updater{start} > $updater{limit});
+ %ut_serv = (%ut_serv, %reset) if (time - $ut_serv{start} > $ut_serv{limit});
+ %oldserv = (%oldserv, %reset) if (time - $oldserv{start} > $oldserv{limit});
+=cut
+ }
+
+ #
+ # Check pending beacons
+ #
+
+ # pending beacons/servers (15 seconds grace time)
+ my $n = $self->get_pending(
+ next_id => $pending{id},
+ added => 15,
+ sort => "id",
+ limit => 1
+ )->[0] if $self->{beacon_checker_enabled};
+
+ # if next pending server/address exists:
+ if ( $n ) {
+ # next pending id will be > $n
+ $pending{id} = $n->{id};
+
+ # query the server using the heartbeat port provided in the beacon/manual add
+ $self->query_udp_server(
+ $n->{id},
+ $n->{ip},
+ $n->{heartbeat},
+ $n->{secure}, # secure string necessary!
+ 1, # request secure challenge
+ );
+
+ # our work is done for this cycle.
+ return;
+ }
+
+ # pending are done and is allowed to reset at a later stadium
+ $pending{c}++;
+
+
+ #
+ # Query Unreal Tournament 99 (demo) servers for serverstats
+ #
+
+ # next server in line
+ $n = $self->get_server(
+ next_id => $ut_serv{id},
+ updated => 3600,
+ gamename => "ut",
+ sort => "id",
+ limit => 1,
+ )->[0] if $self->{utserver_query_enabled};
+
+ # if next server/address exists:
+ if ( $n ) {
+ #next pending id will be > $n
+ $ut_serv{id} = $n->{id};
+
+ # query the server (no secure string)
+ $self->query_udp_server(
+ $n->{id},
+ $n->{ip},
+ $n->{port},
+ "", # no secure string necessary
+ 2, # request full status info
+ );
+
+ # our work is done for this cycle.
+ return;
+ }
+
+ # ut servers are done and is allowed to reset at a later stadium
+ $ut_serv{c}++;
+
+ #
+ # update existing servers (both ut/non-ut)
+ #
+
+ # next server in line
+ $n = $self->get_server(
+ next_id => $updater{id},
+ updated => 7200,
+ sort => "id",
+ limit => 1,
+ )->[0] if $self->{beacon_checker_enabled};
+
+ # if next server/address exists:
+ if ( $n ) {
+ #next pending id will be > $n
+ $updater{id} = $n->{id};
+
+ # query the server (no secure string)
+ $self->query_udp_server(
+ $n->{id},
+ $n->{ip},
+ $n->{port},
+ "", # no secure string necessary
+ 0, # request info
+ );
+
+ # our work is done for this cycle.
+ return;
+ }
+
+ # updating servers is done and is allowed to reset at a later stadium
+ $updater{c}++;
+
+ #
+ # Query servers older than 2 hours
+ #
+
+ # next server in line
+ $n = $self->get_server(
+ next_id => $oldserv{id},
+ before => 7200,
+ sort => "id",
+ limit => 1,
+ )->[0] if $self->{beacon_checker_enabled};
+
+ # if next server/address exists:
+ if ( $n ) {
+ #next old server id will be > $n
+ $oldserv{id} = $n->{id};
+
+ # query the server (no secure string)
+ $self->query_udp_server(
+ $n->{id},
+ $n->{ip},
+ $n->{port},
+ "", # no secure string necessary
+ 0, # request info
+ );
+
+ # our work is done for this cycle.
+ return;
+ }
+
+ # old servers are done and is allowed to reset at a later stadium
+ $oldserv{c}++;
+
+ # and notify about first run being completed
+ if (!defined $self->{firstrun}) {
+ # inform that first run is completed
+ my $t = time-$self->{firstruntime};
+ my $t_readable = ($t > 60) ? (($t/60). ":". ($t%60). "minutes") : ($t. "seconds");
+
+ $self->log("info", "First run completed after $t_readable.");
+ $self->{firstrun} = 0;
+
+ # reset all counters and follow procedure
+ %reset = (start => time, id => 0, c => 0);
+ %pending = (%pending, %reset);
+ %updater = (%updater, %reset);
+ %ut_serv = (%ut_serv, %reset);
+ %oldserv = (%oldserv, %reset);
+ }
+
+ # At this point, we are out of server entries. From here on, just count
+ # down until the cycle is complete and handle new entries while they are
+ # added to the list.
+
+ }
+ );
+
+ # return the timer object to keep it alive outside of this scope
+ return $server_info;
+}
+
+1;
diff --git a/lib/MasterServer/UDP/UpLink.pm b/lib/MasterServer/UDP/UpLink.pm
new file mode 100755
index 0000000..e5c703b
--- /dev/null
+++ b/lib/MasterServer/UDP/UpLink.pm
@@ -0,0 +1,169 @@
+package MasterServer::UDP::UpLink;
+
+use strict;
+use warnings;
+use Encode;
+use AnyEvent::Handle::UDP;
+use Socket qw(sockaddr_in inet_ntoa);
+use Exporter 'import';
+
+our @EXPORT = qw| send_heartbeats
+ do_uplink
+ process_uplink_response
+ process_udp_secure
+ process_udp_basic |;
+
+################################################################################
+## Broadcast heartbeats to other masterservers
+##
+################################################################################
+sub send_heartbeats {
+ my $self = shift;
+
+ # in order to be permitted to sync, you need to share your address too so
+ # others can sync from you too.
+ if ($self->{sync_enabled}) {
+
+ # uplink to every entry of the masterserver brotherhood list
+ foreach my $uplink (values %{$self->masterserver_list()}) {
+ # send uplink
+ $self->do_uplink($uplink->{ip}, $uplink->{udp});
+ }
+ }
+}
+
+
+################################################################################
+## Do an uplink to other 333networks-based masterservers so we can be shared
+## along the 333networks synchronization protocol. Other 333networks-based
+## masterservers are shared in this way too.
+################################################################################
+sub do_uplink {
+ my ($self, $ip, $port) = @_;
+
+ # do not proceed if not all information is available
+ return unless (defined $ip && defined $port && $port > 0);
+
+ # debug spamming
+ $self->log("uplink", "Uplink to Masterserver $ip:$port");
+
+ # connect with UDP server
+ my $udp_client; $udp_client = AnyEvent::Handle::UDP->new(
+ # Bind to this host and port
+ connect => [$ip, $port],
+ timeout => 5,
+ on_timeout => sub {$udp_client->destroy();}, # don't bother reporting timeouts
+ on_error => sub {$udp_client->destroy();}, # or errors
+ on_recv => sub {$self->process_uplink_response(@_)},
+ );
+
+ # Send heardbeat
+ $udp_client->push_send("\\heartbeat\\$self->{beacon_port}\\gamename\\333networks");
+}
+
+################################################################################
+## Process requests received after uplinking
+##
+################################################################################
+sub process_uplink_response {
+ # $self, beacon address, handle, packed client address
+ my ($self, $b, $udp, $pa) = @_;
+
+ # unpack ip from packed client address
+ my ($port, $iaddr) = sockaddr_in($pa);
+ my $peer_addr = inet_ntoa($iaddr);
+
+ # if the beacon has a length longer than a certain amount, assume it is
+ # a fraud or crash attempt
+ if (length $b > 64) {
+ # log
+ $self->log("attack","length exceeded in uplink response: $peer_addr:$port sent $b");
+
+ # truncate and try to continue
+ $b = substr $b, 0, 64;
+ }
+
+ # check if this is a secure challenge
+ $self->process_udp_secure($udp, $pa, $b, $peer_addr)
+ if ($b =~ m/\\secure\\/);
+}
+
+
+################################################################################
+## Process the received secure query and respond with the correct response
+##
+################################################################################
+sub process_udp_secure {
+ # $self, handle, packed address, udp data, peer ip address, $port
+ my ($self, $udp, $pa, $buf, $peer_addr) = @_;
+
+ # received secure in $buf: \basic\secure\l8jfVy
+ my %r;
+ my $raw = $buf; # raw buffer for logging if necessary
+ $buf = encode('UTF-8', $buf);
+ $buf =~ s/\\\\/\\undef\\/;
+ $buf =~ s/\n//;
+ $buf =~ s/\\([^\\]+)\\([^\\]+)/$r{$1}=$2/eg;
+
+ # scope
+ my $response = "";
+
+ # provide basic information if asked for (not uncommon)
+ if (defined $r{basic}) {
+ # compile basic string (identical to process_udp_basic)
+
+ # format: \gamename\ut\gamever\348\minnetver\348\location\0\final\\queryid\16.1
+ $response .= "\\gamename\\333networks"
+ . "\\gamever\\$self->{short_version}"
+ . "\\location\\0"
+ . "\\hostname\\$self->{masterserver_hostname}"
+ . "\\hostport\\$self->{listen_port}";
+ }
+
+ # we only respond with gamename = 333networks
+ if (defined $r{secure}) {
+ # get response
+ $response .= "\\validate\\"
+ . $self->validate_string(gamename => "333networks",
+ enctype => 0,
+ secure => $r{secure});
+ }
+
+ # send the response to the \basic\\secure\wookie query
+ $udp->push_send("$response\\final\\", $pa);
+}
+
+################################################################################
+## Respond to basic or status queries
+## TODO: abstract function for this -- otherwise these functions pile up.
+################################################################################
+sub process_udp_basic {
+ # $self, handle, packed address, udp data, peer ip address, $port
+ my ($self, $udp, $pa, $buf, $peer_addr) = @_;
+
+ # received basic or status in $buf: \basic\ or \status\
+ my %r;
+ $buf = encode('UTF-8', $buf);
+ $buf =~ s/\\\\/\\undef\\/;
+ $buf =~ s/\n//;
+ $buf =~ s/\\([^\\]+)\\([^\\]+)/$r{$1}=$2/eg;
+
+ # scope
+ my $basic = "";
+ # provide basic information
+
+ if (defined $r{basic} || defined $r{status} || defined $r{info}) {
+ # compile basic string (identical to process_udp_basic)
+
+ # format: \gamename\ut\gamever\348\minnetver\348\location\0\final\\queryid\16.1
+ $basic = "\\gamename\\333networks"
+ . "\\gamever\\$self->{short_version}"
+ . "\\location\\0"
+ . "\\hostname\\$self->{masterserver_hostname}"
+ . "\\hostport\\$self->{listen_port}";
+ }
+
+ # send the response to the \basic\ or \status\
+ $udp->push_send("$basic\\final\\", $pa);
+}
+1;