diff options
Diffstat (limited to 'lib/MasterServer/UDP')
| -rwxr-xr-x | lib/MasterServer/UDP/BeaconCatcher.pm | 15 | ||||
| -rwxr-xr-x | lib/MasterServer/UDP/BeaconChecker.pm | 147 | ||||
| -rwxr-xr-x | lib/MasterServer/UDP/DatagramProcessor.pm | 245 | ||||
| -rwxr-xr-x | lib/MasterServer/UDP/UCCAppletQuery.pm | 86 | ||||
| -rwxr-xr-x | lib/MasterServer/UDP/UDPTicker.pm | 298 | ||||
| -rwxr-xr-x | lib/MasterServer/UDP/UpLink.pm | 169 |
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; |
