aboutsummaryrefslogtreecommitdiff
path: root/lib/MasterServer/Core
diff options
context:
space:
mode:
Diffstat (limited to 'lib/MasterServer/Core')
-rwxr-xr-xlib/MasterServer/Core/Core.pm99
-rwxr-xr-xlib/MasterServer/Core/Logging.pm5
-rwxr-xr-xlib/MasterServer/Core/Schedulers.pm125
-rwxr-xr-xlib/MasterServer/Core/Secure.pm128
-rwxr-xr-xlib/MasterServer/Core/Stats.pm39
-rwxr-xr-xlib/MasterServer/Core/Util.pm89
-rwxr-xr-xlib/MasterServer/Core/Version.pm11
7 files changed, 368 insertions, 128 deletions
diff --git a/lib/MasterServer/Core/Core.pm b/lib/MasterServer/Core/Core.pm
index a20172c..3f99a3a 100755
--- a/lib/MasterServer/Core/Core.pm
+++ b/lib/MasterServer/Core/Core.pm
@@ -7,7 +7,7 @@ use AnyEvent;
use Exporter 'import';
use DBI;
-our @EXPORT = qw | halt main |;
+our @EXPORT = qw | halt select_database_type main |;
################################################################################
## Handle shutting down the program in case a fatal error occurs.
@@ -34,6 +34,19 @@ sub halt {
}
################################################################################
+## Set up the database connection
+################################################################################
+sub select_database_type {
+ my $self = shift;
+
+ # Connect to database
+ $self->{dbh} = $self->database_login();
+
+ # and test whether we succeeded.
+ $self->halt() unless (defined $self->{dbh});
+}
+
+################################################################################
## Initialize all processes and start various functions
################################################################################
sub main {
@@ -42,67 +55,69 @@ sub main {
# condition var prevents or allows the program from ending
$self->{must_halt} = AnyEvent->condvar;
- # determine version info
+ # force version info
$self->version();
+ # print startup
+ print "Running...\n";
+
# keep several objects alive outside their original scope
$self->{scope} = ();
# startup procedure information
+ $self->log("info", "");
+ $self->log("info", "");
$self->log("info", "333networks Master Server Application.");
- $self->log("info", "Build: $self->{build_type}");
- $self->log("info", "Version: $self->{build_version}");
- $self->log("info", "Written by $self->{build_author}");
- $self->log("info", "Logs are written to $self->{log_dir}");
+ $self->log("info", "Hostname: $self->{masterserver_hostname}");
+ $self->log("info", "Build: $self->{build_type}");
+ $self->log("info", "Version: $self->{build_version}");
+ $self->log("info", "Author: $self->{build_author}");
+ $self->log("info", "Logs: $self->{log_dir}");
# determine the type of database and load the appropriate module
- {
- # read from login
- my @db_type = split(':', $self->{dblogin}->[0]);
-
- # format supported?
- if ( "Pg SQLite mysql" =~ m/$db_type[1]/i) {
-
- # inform us what DB we try to load
- $self->log("load","Loading $db_type[1] database module.");
-
- # load dbd and tables/queries for this db type
- MasterServer::load_recursive("MasterServer::Database::$db_type[1]");
-
- # Connect to database
- $self->{dbh} = $self->database_login();
-
- # and test whether we succeeded.
- $self->halt() unless (defined $self->{dbh});
- }
- else {
- # raise error and halt
- $self->log("fatal", "The masterserver could not determine the chosen database type.");
- $self->halt();
- }
- }
+ $self->select_database_type();
+
+ #
+ # Prepare necessary tasks for running the masterserver
+ #
# (re)load the list with ciphers from the config file, into the database
$self->load_ciphers();
+ # set first run flag to avoid ignoring servers after downtime
+ $self->{firstrun} = undef;
+ $self->{firstruntime} = time;
+
+ ###
+ #
+ # activate all schedulers and functions
+ #
+ ###
+
+ #
+ # Timers
+ #
+ # tasks that are executed once or twice per hour
+ $self->{scope}->{long_periodic_tasks} = $self->long_periodic_tasks();
+ #
+ # tasks that are executed every few minutes
+ $self->{scope}->{short_periodic_tasks} = $self->short_periodic_tasks();
+ #
+ # tasks that are executed every few milliseconds
+ $self->{scope}->{udp_ticker} = $self->udp_ticker();
+
+ #
+ # Network listeners
+ #
# start the listening service (listen for UDP beacons)
$self->{scope}->{beacon_catcher} = $self->beacon_catcher();
-
- # start the beacon checker service (query entries from the pending list)
- $self->{scope}->{beacon_checker} = $self->beacon_checker() if ($self->{beacon_checker_enabled});
-
+ #
# provide server lists to clients with the browser host server
$self->{scope}->{browser_host} = $self->browser_host();
- # query other masterserver applets to get more server addresses
- $self->{scope}->{ucc_applet_query} = $self->ucc_applet_query_scheduler() if ($self->{master_applet_enabled});
-
- # synchronize with 333networks-based masterservers
- $self->{scope}->{syncer_scheduler} = $self->syncer_scheduler() if ($self->{sync_enabled});
-
# all modules loaded. Running...
$self->log("info", "All modules loaded. Masterserver is now running.");
-
+
# prevent main program from ending prematurely
$self->{must_halt}->recv;
}
diff --git a/lib/MasterServer/Core/Logging.pm b/lib/MasterServer/Core/Logging.pm
index 800fa43..5aaf683 100755
--- a/lib/MasterServer/Core/Logging.pm
+++ b/lib/MasterServer/Core/Logging.pm
@@ -15,6 +15,9 @@ our @EXPORT = qw| log |;
sub log {
my ($self, $type, $msg) = @_;
+ # flush
+ $| = 1;
+
# parse time of log entry and prep for rotating log
my $time = strftime('%Y-%m-%d %H:%M:%S',localtime);
my $yearly = strftime('-%Y',localtime);
@@ -26,7 +29,7 @@ sub log {
return if (defined $type && $self->{suppress} =~ m/$type/i);
# determine filename
- my $f = "MasterServer-Perl";
+ my $f = "MasterServer";
# rotate log filename according to config
$f .= $daily if ($self->{log_rotate} =~ /^daily$/i );
diff --git a/lib/MasterServer/Core/Schedulers.pm b/lib/MasterServer/Core/Schedulers.pm
new file mode 100755
index 0000000..97e45a5
--- /dev/null
+++ b/lib/MasterServer/Core/Schedulers.pm
@@ -0,0 +1,125 @@
+
+package MasterServer::Core::Schedulers;
+
+use strict;
+use warnings;
+use AnyEvent;
+use POSIX qw/strftime/;
+use Exporter 'import';
+use DBI;
+
+our @EXPORT = qw |
+ long_periodic_tasks
+ short_periodic_tasks
+|;
+
+################################################################################
+## tasks that are executed only once or twice per hour
+################################################################################
+sub long_periodic_tasks {
+ my $self = shift;
+ my $num = 0;
+
+ return AnyEvent->timer (
+ after => 300, # 5 minutes grace time
+ interval => 1800, # execute every half hour
+ cb => sub {
+
+ ## update Killing Floor stats
+ $self->read_kfstats();
+
+ # time spacer
+ my $t = 0;
+
+ # clean out handles from the previous round (executed or not)
+ $self->{scope}->{sync} = ();
+
+ ## Query Epic Games'-based UCC applets periodically to get an additional
+ ## list of online UT, Unreal (or other) game servers.
+ if ($self->{master_applet_enabled}) {
+ for my $ms (@{$self->{master_applet}}) {
+
+ # add 3 second delay to spread network/server load
+ $self->{scope}->{sync}->{$t} = AnyEvent->timer(
+ after => 3*$t++,
+ cb => sub{$self->query_applet($ms)}
+ );
+ }
+ }
+
+ # do NOT reset $t, keep padding time -- you should not have more than 600
+ # entries in applets/syncer in total.
+
+ ## Request the masterlist for selected or all games from other
+ ## 333networks-based masterservers that uplinked to us and otherwise made
+ ## our list (config, manual entry, etc)
+ if ($self->{sync_enabled}) {
+ foreach my $ms (values %{$self->masterserver_list()}) {
+
+ # add 3 second delay to spread network/server load
+ $self->{scope}->{sync}->{$t} = AnyEvent->timer(
+ after => 3*$t++,
+ cb => sub{$self->sync_with_master($ms) if ($ms->{tcp} > 0)}
+ );
+ }
+ }
+
+ #
+ # Also very long-running tasks, like once per day:
+ #
+ if ($num++ >= 47) {
+ # reset counter
+ $num = 0;
+
+ #
+ # do database dump
+ #
+ my $time = strftime('%Y-%m-%d-%H-%M',localtime);
+
+ # read db type from db login
+ my @db_type = split(':', $self->{dblogin}->[0]);
+ $db_type[2] =~ s/dbname=//;
+
+ if ($db_type[1] eq "Pg") {
+ # use pg_dump to dump Postgresql databases
+ system("pg_dump $db_type[2] -U $self->{dblogin}->[1] > $self->{root}/data/dumps/$db_type[1]-$time.db");
+ $self->log("dump", "Dumping database to /data/dumps/$db_type[1]-$time.db");
+ }
+ }
+
+ },
+ );
+}
+
+################################################################################
+## tasks that are executed every few minutes
+################################################################################
+sub short_periodic_tasks {
+ my $self = shift;
+
+ return AnyEvent->timer (
+ after => 10,
+ interval => 120,
+ cb => sub {
+
+ ## update stats on direct beacons and total number of servers
+ $self->update_stats();
+
+ ## determine whether servers are still uplinking to us. If not, toggle.
+ $self->write_direct_beacons() if (defined $self->{firstrun});
+
+ ## delete old servers from the "pending" list (except for the first run)
+ $self->delete_old_pending() if (defined $self->{firstrun});
+
+ ## uplink to other 333networks masterservers with heartbeats,
+ ## that way we can index other masterservers too
+ $self->send_heartbeats();
+
+ #
+ # more short tasks?
+ #
+ },
+ );
+}
+
+1;
diff --git a/lib/MasterServer/Core/Secure.pm b/lib/MasterServer/Core/Secure.pm
index 07e48b8..51d1832 100755
--- a/lib/MasterServer/Core/Secure.pm
+++ b/lib/MasterServer/Core/Secure.pm
@@ -8,11 +8,8 @@ use Exporter 'import';
our @EXPORT = qw| load_ciphers
secure_string
- validated_beacon
- validated_request
- validate_string
- charshift
- get_validate_string |;
+ validate_string
+ compare_challenge |;
################################################################################
## Supported Games list ciphers
@@ -21,7 +18,6 @@ our @EXPORT = qw| load_ciphers
## data/supportedgames.pl file.
##
## Only config files after 5 October 2015 work with this script.
-## IMPORTANT! Afterwards, the $self->{game} hash will be cleared!
################################################################################
sub load_ciphers {
my $self = shift;
@@ -47,14 +43,13 @@ sub load_ciphers {
$self->{dbh}->rollback;
$self->halt();
}
-
}
# commit
$self->{dbh}->commit;
$self->log("info", "Cipher database successfully updated!");
- # unload the game variables from memory
+ # unload the game variables from masterserver memory
$self->{game} = undef;
}
@@ -78,82 +73,54 @@ sub secure_string {
# authenticate the \validate\ response for the \secure\ challenge.
# returns 1 on valid response, 0 on invalid
################################################################################
-sub validated_beacon {
- my ($self, $gamename, $secure, $enctype, $validate) = @_;
+sub compare_challenge {
+ my ($self, %o) = @_;
# debugging enabled? Then don't care about validation
return 1 if ($self->{debug_validate});
- # enctype given?
- $enctype = 0 unless $enctype;
+ # secure string too long? (because vulnerable in UE)
+ return 0 if (length $o{secure} > 16);
- if ($self->{ignore_beacon_key} =~ m/$gamename/i){
- $self->log("secure", "ignored beacon validation for $gamename");
- return 1;
- }
+ # additional conditions to skip checking provided?
+ $o{ignore} = "" unless $o{ignore};
- # compare received response with expected response
- return ($self->validate_string($gamename, $secure, $enctype) eq $validate) || 0;
-}
+ # ignore this game if asked to do so
+ if ($o{ignore} =~ m/$o{gamename}/i){
+ $self->log("secure", "ignored beacon validation for $o{gamename}");
+ return 1;
+ }
-################################################################################
-# authenticate the \validate\ response for the \secure\ challenge.
-# returns 1 on valid response, 0 on invalid
-################################################################################
-sub validated_request {
- my ($self, $gamename, $secure, $enctype, $validate) = @_;
-
- # debugging enabled? Then don't care about validation
- return 1 if ($self->{debug_validate});
+ # enctype given?
+ $o{enctype} = 0 unless $o{enctype};
- # enctype given?
- $enctype = 0 unless $enctype;
+ # get cipher corresponding with the gamename
+ my $cip = $self->get_game_props($o{gamename})->{cipher};
- # ignore games and beacons that are listed
- if ($self->{ignore_browser_key} =~ m/$gamename/i){
- $self->log("secure", "ignored browser validation for $gamename");
- return 1;
- }
-
- # compare received response with expected response
- return ($self->validate_string($gamename, $secure, $enctype) eq $validate) || 0;
+ # calculate validate string
+ my $val = get_validate_string($cip, $o{secure}, $o{enctype});
+
+ # return whether or not they match
+ return ($val eq $o{validate});
}
################################################################################
-# process the validate string as a response to the secure challenge
-# returns: validate string (usually 8 characters long)
+# obtain the secure/validate challenge string
################################################################################
sub validate_string {
- my ($self, $game, $sec, $enc) = @_;
+ my ($self, %o) = @_;
- # get cipher from gamename
- my $cip = $self->get_cipher($game);
+ # secure string too long? (because vulnerable in UE)
+ return 0 if (length $o{secure} > 16);
- # don't accept challenge longer than 16 characters (because vulnerable in UE)
- if (length $sec > 16) {
- return "invalid!"}
+ # get cipher from gamename
+ my $cip = $self->get_game_props(lc $o{gamename})->{cipher};
- # check for valid encryption choices
- my $enc_val = (defined $enc && 0 <= $enc && $enc <= 2) ? $enc : 0;
+ # enctype given?
+ $o{enctype} = 0 unless $o{enctype};
# calculate and return validate string
- return $self->get_validate_string($cip, $sec, $enc_val);
-}
-
-################################################################################
-# rotate characters as part of the secure/validate algorithm.
-# arg and return: int (representing a character)
-################################################################################
-sub charshift {
- my ($self, $reg) = @_;
- return($reg + 65) if ($reg < 26);
- return($reg + 71) if ($reg < 52);
- return($reg - 4) if ($reg < 62);
- return(43) if ($reg == 62);
- return(47) if ($reg == 63);
-
- # if all else fails
- return(0);
+ return get_validate_string($cip, $o{secure}, $o{enctype});
}
################################################################################
@@ -166,14 +133,15 @@ sub charshift {
# was released under the GNU General Public License, for more information, see
# the original software at http://aluigi.altervista.org/papers.htm#gsmsalg
#
-# conversion and modification of the algorithm by Darkelarious, June 2014.
+# conversion and modification of the algorithm by Darkelarious, June 2014 with
+# explicit, written permission of Luigi Auriemma.
#
# args: game cipher, 6-char challenge string, encryption type
# returns: validate string (usually 8 characters long)
# !! requires cipher hash to be configured in config! (imported or otherwise)
################################################################################
sub get_validate_string {
- my ($self, $cipher_string, $secure_string, $enctype) = @_;
+ my ($cipher_string, $secure_string, $enctype) = @_;
# use pre-built rotations for enctype
# -- see GSMSALG 0.3.3 reference for copyright and more information
@@ -245,7 +213,7 @@ sub get_validate_string {
$tmp[$i] = $sec[$i] ^ $enc[($l + $m) & 0xff];
}
-# part of the enctype 1-2 process
+ # part of the enctype 1-2 process
for($sec_len = $i; $sec_len % 3; $sec_len++) {
$tmp[$sec_len] = 0;
}
@@ -270,10 +238,10 @@ sub get_validate_string {
$m = $m % 256;
$n = $tmp[$i + 2];
$n = $n % 256;
- $val[$p++] = $self->charshift($l >> 2);
- $val[$p++] = $self->charshift((($l & 3 ) << 4) | ($m >> 4));
- $val[$p++] = $self->charshift((($m & 15) << 2) | ($n >> 6));
- $val[$p++] = $self->charshift($n & 63);
+ $val[$p++] = charshift($l >> 2);
+ $val[$p++] = charshift((($l & 3 ) << 4) | ($m >> 4));
+ $val[$p++] = charshift((($m & 15) << 2) | ($n >> 6));
+ $val[$p++] = charshift($n & 63);
}
# return to ascii characters
@@ -283,4 +251,20 @@ sub get_validate_string {
return $str;
}
+################################################################################
+# rotate characters as part of the secure/validate algorithm.
+# arg and return: int (representing a character)
+################################################################################
+sub charshift {
+ my $reg = shift;
+ return($reg + 65) if ($reg < 26);
+ return($reg + 71) if ($reg < 52);
+ return($reg - 4) if ($reg < 62);
+ return(43) if ($reg == 62);
+ return(47) if ($reg == 63);
+
+ # if all else fails
+ return(0);
+}
+
1;
diff --git a/lib/MasterServer/Core/Stats.pm b/lib/MasterServer/Core/Stats.pm
new file mode 100755
index 0000000..25044e8
--- /dev/null
+++ b/lib/MasterServer/Core/Stats.pm
@@ -0,0 +1,39 @@
+
+package MasterServer::Core::Stats;
+
+use strict;
+use warnings;
+use AnyEvent::IO;
+use Exporter 'import';
+
+our @EXPORT = qw| update_stats |;
+
+################################################################################
+# Update statistics on servers and update the games table with those values
+################################################################################
+sub update_stats {
+ my $self = shift;
+
+ # get all gamenames where there is one or more servers online and update the
+ # stats per gamename.
+ my $games = $self->get_gamelist_stats();
+
+ # iterate through available stats
+ for my $e (@{$games}) {
+
+ # extract gamename, number of direct uplinks and total servers
+ my %opt = ();
+ $opt{gamename} = $e->[0];
+ $opt{num_uplink} = $e->[1];
+ $opt{num_total} = $e->[2];
+
+ # write to DB
+ $self->write_stat(%opt);
+ }
+
+ #done
+ $self->log("stat", "Updated all game statistics.");
+
+}
+
+1;
diff --git a/lib/MasterServer/Core/Util.pm b/lib/MasterServer/Core/Util.pm
index af3a551..4f64fe1 100755
--- a/lib/MasterServer/Core/Util.pm
+++ b/lib/MasterServer/Core/Util.pm
@@ -4,10 +4,11 @@ package MasterServer::Core::Util;
use strict;
use warnings;
use IP::Country::Fast;
+use Socket;
use POSIX qw/strftime/;
use Exporter 'import';
-our @EXPORT = qw| ip2country countryflag valid_address |;
+our @EXPORT = qw| ip2country host2ip valid_address db_all sqlprint |;
################################################################################
## return the abbreviated country name based on IP
@@ -19,23 +20,24 @@ sub ip2country {
}
################################################################################
-## return the flag of a country
+## return IP of a hostname
################################################################################
-sub countryflag {
- my ($self, $country) = @_;
- # placeholder function to return the flag of a country
+sub host2ip {
+ my ($self, $name) = @_;
+ return inet_ntoa(inet_aton($name)) if $name;
}
+
################################################################################
## Verify whether a given domain name or IP address and port are valid.
-## returns 1/0 if valid/invalid ip + port
+## returns 1/0 if valid/invalid ip + port. IPv4 ONLY!
################################################################################
sub valid_address {
my ($self, $a, $p) = @_;
-
+
# check if ip and port are in valid range
- my $val_addr = ($a =~ '\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b');
- my $val_port = (0 < $p && $p <= 65535);
+ my $val_addr = ($a =~ '\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b') if $a;
+ my $val_port = (0 < $p && $p <= 65535) if $p;
# exclude addresses where we don't want people sniffing
for (qw|192.168.(.\d*).(.\d*) 127.0.(.\d*).(.\d*) 10.0.(.\d*).(.\d*)|){$val_addr = 0 if ($a =~ m/$_/)}
@@ -44,4 +46,73 @@ sub valid_address {
return ($val_addr && $val_port);
}
+################################################################################
+# Adaptation of TUWF's dbAll sql function
+################################################################################
+sub db_all {
+ my $self = shift;
+ my $sqlq = shift;
+ my $s = $self->{dbh};
+
+ $sqlq =~ s/\r?\n/ /g;
+ $sqlq =~ s/ +/ /g;
+ my(@q) = @_ ? sqlprint($sqlq, @_) : ($sqlq);
+
+ my($q, $r);
+ my $ret = eval {
+ $q = $s->prepare($q[0]);
+ $q->execute($#q ? @q[1..$#q] : ());
+ $r = $q->fetchall_arrayref({});
+ $q->finish();
+ 1;
+ };
+
+ $r = [] if (!$r || ref($r) ne 'ARRAY');
+ return $r;
+}
+
+################################################################################
+# sqlprint (TUWF):
+# ? normal placeholder
+# !l list of placeholders, expects arrayref
+# !H list of SET-items, expects hashref or arrayref: format => (bind_value || \@bind_values)
+# !W same as !H, but for WHERE clauses (AND'ed together)
+# !s the classic sprintf %s, use with care
+# This isn't sprintf, so all other things won't work,
+# Only the ? placeholder is supported, so no dollar sign numbers or named placeholders
+################################################################################
+sub sqlprint { # query, bind values. Returns new query + bind values
+
+ my @a;
+ my $q='';
+ for my $p (split /(\?|![lHWs])/, shift) {
+ next if !defined $p;
+ if($p eq '?') {
+ push @a, shift;
+ $q .= $p;
+ } elsif($p eq '!s') {
+ $q .= shift;
+ } elsif($p eq '!l') {
+ my $l = shift;
+ $q .= join ', ', map '?', 0..$#$l;
+ push @a, @$l;
+ } elsif($p eq '!H' || $p eq '!W') {
+ my $h=shift;
+ my @h=ref $h eq 'HASH' ? %$h : @$h;
+ my @r;
+ while(my($k,$v) = (shift(@h), shift(@h))) {
+ last if !defined $k;
+ my($n,@l) = sqlprint($k, ref $v eq 'ARRAY' ? @$v : $v);
+ push @r, $n;
+ push @a, @l;
+ }
+ $q .= ($p eq '!W' ? 'WHERE ' : 'SET ').join $p eq '!W' ? ' AND ' : ', ', @r
+ if @r;
+ } else {
+ $q .= $p;
+ }
+ }
+ return($q, @a);
+}
+
1;
diff --git a/lib/MasterServer/Core/Version.pm b/lib/MasterServer/Core/Version.pm
index ccf9491..bf12f21 100755
--- a/lib/MasterServer/Core/Version.pm
+++ b/lib/MasterServer/Core/Version.pm
@@ -23,17 +23,20 @@ sub version {
# these fields does not count as a significant alteration.
#
# -- addition to the LICENCE, you are only allowed to modify these lines
- # if you send Darkelarious a postcard or email with your compliments.
+ # if you send Darkelarious a postcard or (e)mail with your compliments.
#
# master type
- $self->{build_type} = "333networks Masterserver-Perl (Pg-SQLite-MySQL) 20151108209";
+ $self->{build_type} = "333networks Masterserver-Perl pre-release";
# version
- $self->{build_version} = "2.0.9";
+ $self->{build_version} = "2.2.4";
+
+ # short version for uplinks
+ $self->{short_version} = "MS-perl $self->{build_version}";
# date yyyy-mm-dd
- $self->{build_date} = "2015-11-08";
+ $self->{build_date} = "2016-11-17";
#author, email
$self->{build_author} = "Darkelarious, darkelarious\@333networks.com";