diff options
25 files changed, 1251 insertions, 206 deletions
@@ -20,13 +20,13 @@ DESCRIPTION REQUIREMENTS - Postgresql or SQLite3 - Perl 5.10 or above - - The following CPAN modules - FCGI - DBI - DBD::Pg or DBD::SQLite - AnyEvent - AnyEvent::Handle::UDP - IP::Country + - The following CPAN modules: + FCGI + DBI + DBD::Pg or DBD::SQLite + AnyEvent + AnyEvent::Handle::UDP + IP::Country - screen (or another terminal multiplexer, optional) INSTALL @@ -36,21 +36,23 @@ INSTALL This repository consists of Perl modules and is run in terminal. The software utilizes UDP and TCP connections to receive and send information about game - servers. Carefully read through - the code before you start randomly querying other game servers and/or - masterservers. + servers. Carefully read through the code and documentation before you start + randomly querying other game servers and/or masterservers. The masterserver stores server addresses in the database. This database must - be created manually. The tables to be created can be found in the sql in - the folder "data/database". Support for multiple database drivers is slowly - being added. Use the database type of choice and created the tables. + be created manually. SQLite3 users should not forget to chmod the database + file with read/write access. The tables to be created can be found in the sql + in the folder "data/database". Support for multiple database drivers is + slowly being added. Use the database type of choice and created the tables. CONFIGURATION + The 333networks masterserver comes with options. These options are found in configuration file "data/masterserver-config.pl". Comments in this file give a brief description. Below, the configuration is discussed in further detail. Masterserver HOST information + Fill in your contact details here if you want to be able to synchronize with other masterservers. Though the synchronization process also works without your contact details, it is preferred and appreciated if you allow other @@ -60,6 +62,7 @@ CONFIGURATION build you are running and which games you currently support. Database login information + The masterserver slowly starts to support more and more different database types. In the module "lib/MasterServer/Databases" you can see which types are currently working. Examples in use (only one option should be used): @@ -68,36 +71,40 @@ CONFIGURATION SQLite: ["dbi:SQLite:dbname=$ROOT/data/database_name.db",'',''] Logging + All events are logged by default. Every new beacon, database transaction or serverlist request is recorded in the logfile. For debugging purposes, this is - very useful, but during regular use, the logfile will grow big very fast. With + very useful, but for regular use, the logfile will grow big very fast. With the "suppress" option, log messages can be suppressed from being logged. This option takes the message type, separated by spaces. To suppress a message type, use the identifier between brackets. Example: - [2015-01-31 17:31:47] [Success] > Connected to the Postgres database. + [2015-05-19 17:31:47] [load] > Connected to the Postgres database. In this message, the timestamp of the message is shown first, followed by the identifier "Success". To suppress this type of message, the following parameter can be set: - suppress => " Success " + suppress => " load " + suppress => " load beacon secure hostname " - More message types can be suppressed, where the types are seperated by spaces. + More message types can be suppressed, where the types are separated by spaces. If you want to log a lot of events, you could consider rotating the logs every day, week, month or year. The 'log_rotate' allows you to store events in different files. Network settings + The masterserver uses UDP and TCP networking. The default port numbers are 27900 for server beacons (UDP) and 28900 for serverlists (TCP). Home-hosters have to open those firewall ports. Not all games request data in the same format. Until we established how the - request for a different datatype is made, we also provide the ability to + request for a different datatype are made, we also provide the ability to send data in the alternate (byte) form manually. More games can be added, - seperated by spaces. + separated by spaces. Supported Games & Secure/Validate + All GameSpy protocol games communicate according to a protocol that requires servers and clients to authenticate each other. As far as 333networks are concerned, the authentication ciphers (keys) are confidential and intellectual @@ -117,6 +124,7 @@ CONFIGURATION the "ignore_key" options. Timer & Enable settings + There are three methods to add server addresses to the list: by receiving a direct beacon, by validating a pending server and by synchronization. Some of these functions can be disabled from the configuration. @@ -138,6 +146,7 @@ CONFIGURATION file are listed in [s]econds. Query UCC Applets + In addition, other UCC masterserver applets can be queried for more server addresses. This should not be done without permission of the UCC applet hosts, it's impolite to do so without asking. The new servers are added to @@ -146,18 +155,22 @@ CONFIGURATION work properly. Change with "master_applet_enabled". Synchronization settings + Synchronization between masterservers allows you to receive the list from other masterservers, but in return, you allow other masterservers to query you too, with the same request. If you do not wish this to happen, synchronization can be disabled. Newly received servers are added to the "pending" list as - described above. Change with "sync_enabled". + described above. Change with "sync_enabled". Please note that this will not + prevent others from obtaining the server lists. Attempting to disable this is + hypocrite and ambiguous, as "regular" clients do the same. RUNNING + After all CPAN modules have been installed and all options have been reviewed in the configuration file, the masterserver can be started with the following command: - screen -dmS "UTMSE" ./util/masterserver.pl + screen -dmS "masterserver" ./util/masterserver.pl Keep in mind that this configuration of database, masterserver and website is designed for 333networks. If you want to set up your own system for any diff --git a/data/database/tables-SQLite.sql b/data/database/tables-SQLite.sql index 8ed9471..37431bb 100755 --- a/data/database/tables-SQLite.sql +++ b/data/database/tables-SQLite.sql @@ -26,7 +26,7 @@ CREATE TABLE pending( ); CREATE TABLE server_info( - FOREIGN KEY(server_id) REFERENCES serverlist(id), + server_id INTEGER, minnetver INTEGER NOT NULL DEFAULT 400, gamever INTEGER NOT NULL DEFAULT 400, location INTEGER NOT NULL DEFAULT 0, @@ -52,7 +52,8 @@ CREATE TABLE server_info( timelimit INTEGER NOT NULL DEFAULT 0, goalteamscore INTEGER NOT NULL DEFAULT 0, fraglimit INTEGER NOT NULL DEFAULT 0, - mutators TEXT NOT NULL DEFAULT 'None' + mutators TEXT NOT NULL DEFAULT 'None', + FOREIGN KEY(server_id) REFERENCES serverlist(id) ); CREATE TABLE player_info( diff --git a/data/masterserver-config.pl b/data/masterserver-config.pl index 0755672..e8fe418 100755 --- a/data/masterserver-config.pl +++ b/data/masterserver-config.pl @@ -16,26 +16,27 @@ our %S = ( # example: 333networks -- http://master.333networks.com -- info@333networks.com contact_details => '333networks -- http://master.333networks.com -- info@333networks.com', - # host address (and tcp port) - masterserver_address => 'master.333networks.com:28900', + # host address + masterserver_address => 'master.333networks.com', ################################################################################ # Database Login Configuration # # # # Login credentials for the database that was created manually before. # +# Yes, that means that you need to create the database and tables on your own. # # Use only one option: Postgresql, SQLite (or future: MySQL) # # # ################################################################################ # Postgresql - dblogin => ['dbi:Pg:dbname=testdatabase', 'unrealmaster', 'unrealmasterpassword'], + dblogin => ['dbi:Pg:dbname=database_name', 'username', 'password'], # SQLite #dblogin => ["dbi:SQLite:dbname=$ROOT/data/database_name.db",'',''], # MySQL #dblogin => ["dbi:mysql:database=database_name;host=localhost;port=3306",'user','password'], - + ################################################################################ # Logging configuration # # # @@ -44,30 +45,35 @@ our %S = ( # # ################################################################################ - # log file location + # log file location (folder name!) log_dir => "$ROOT/log/", - #rotate log? options: daily, weekly, monthly, yearly, none + #new log for every period of time? options: daily, weekly, monthly, yearly, none log_rotate => "weekly", # print to screen (1=yes, 0=no) printlog => 1, # which messages do you NOT want to see in the logs (and screen)? - suppress => "debug_spam load hostname udp add update delete secure beacon", - #suppress => "none", + suppress => "none", # show all entries + + # suppress the most annoying messages + #suppress => "add update delete read tcp udp query secure hostname", + + # print database errors + db_print => 0, ################################################################################ # Network settings # # # -# Beacon UDP port and Browser TCP port # +# Beacon UDP port (beacons) and Browser TCP port (serverlist) # # Settings for games that require different data formats # # # ################################################################################ # port settings listen_port => 28900, # default 28900 - beacon_port => 28906, # default 27900 + beacon_port => 27900, # default 27900 # these games require a special hex format instead of \ip\ip:port\ hex_format => "bcommander", @@ -79,7 +85,7 @@ our %S = ( # # ################################################################################ - # Disable checks, all games pass as validated. (0=validate, 1=allow all) + # disable checks, all games pass as validated (0=validate only, 1=don't check) debug_validate => 0, # accept only servers that pass the secure/validate challenge, takes longer @@ -97,28 +103,30 @@ our %S = ( # Wait 60+ seconds before starting timers for incoming beacons # # # ################################################################################ - - # Synchronization with other 333networks-based masterservers - sync_enabled => 1, - sync_time => [60, 1200], # Query UCC-based applets master_applet_enabled => 1, - master_applet_time => [70, 600], + master_applet_time => [90, 1200], + + # Synchronization with other 333networks-based masterservers + sync_enabled => 1, # 0 = disabled + sync_time => [180, 1200], # Beacon Checker query all addresses in the database, requesting "basic" and # "info". Execute at least twice per hour, to avoid time-outs in own data. - # disabling may break support for certain games. + # disabling breaks support for certain games [citation needed]. beacon_checker_enabled => 1, - beacon_checker_time => [80, 0.5, 1800], + beacon_checker_time => [60, 0.5, 1800], # Collect server information for the 333networks main site. Identical - # mechanism as the Beacon Checker. Disable when not interested in UT info. + # mechanism as the Beacon Checker. Disable when not interested in UT info + # for your website. + # NB: with some work it can be adapted to work with any other game. Own risk. utserver_query_enabled => 1, - utserver_query_time => [90, 0.15, 240], + utserver_query_time => [75, 0.15, 240], # Maintenance duties like cleaning out old servers/players - maintenance_time => [3600, 60], + maintenance_time => [3600, 300], ################################################################################ # Synchronization settings # @@ -154,6 +162,7 @@ our %S = ( {ip => "utmaster.epicgames.com", port => 28900, game => "ut"}, {ip => "master.hypercoop.tk", port => 28900, game => "unreal"}, {ip => "master.newbiesplayground.net", port => 28900, game => "unreal"}, + {ip => "master.hlkclan.net", port => 28900, game => "unreal"}, ], ); #end %S @@ -166,15 +175,7 @@ our %S = ( # adding a game does not necessarily mean that suddenly the protocol will # # be supported. # # # -# Import either the first or the second file; the first one is a sample file # -# that holds all the game names, but not their keys (public version). The # -# other file is not included in this git and contains both game names and # -# their confidential ciphers. # -# # -# Importing both may give some unstable "invalid gamename" checks. # -# # ################################################################################ -#require "$ROOT/data/supportedgames.pl"; -require "/server/Repositories/supportedgames.pl"; +require "$ROOT/data/supportedgames.pl"; 1; diff --git a/lib/MasterServer/Core/Core.pm b/lib/MasterServer/Core/Core.pm index e4e4836..0e0cd58 100755 --- a/lib/MasterServer/Core/Core.pm +++ b/lib/MasterServer/Core/Core.pm @@ -11,20 +11,26 @@ our @EXPORT = qw | halt main |; ################################################################################ ## Handle shutting down the program in case a fatal error occurs. -## TODO: lockfile! ################################################################################ sub halt { my $self = shift; # log shutdown - $self->log("stop", "Stopping the masterserver now."); + $self->log("stop", "Stopping the masterserver."); # clear all other timers, network servers, etc $self->{dbh}->disconnect() if (defined $self->{dbh}); + $self->{dbh} = undef; $self->{scope} = undef; - # and send signal to condition var + # and send signal to condition var to let the loops end $self->{must_halt}->send; + + # log halt + $self->log("stop", "Shutting down NOW!"); + + # time for a beer. + exit; } ################################################################################ @@ -81,20 +87,21 @@ sub main { # 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; - $self->log("stop", "Shutting down NOW!"); - - # time for a beer. - exit; } 1; diff --git a/lib/MasterServer/Core/Logging.pm b/lib/MasterServer/Core/Logging.pm index c503e1a..800fa43 100755 --- a/lib/MasterServer/Core/Logging.pm +++ b/lib/MasterServer/Core/Logging.pm @@ -26,7 +26,7 @@ sub log { return if (defined $type && $self->{suppress} =~ m/$type/i); # determine filename - my $f = "MasterServer-333networks"; + my $f = "MasterServer-Perl"; # rotate log filename according to config $f .= $daily if ($self->{log_rotate} =~ /^daily$/i ); diff --git a/lib/MasterServer/Core/Util.pm b/lib/MasterServer/Core/Util.pm index eb4d509..af3a551 100755 --- a/lib/MasterServer/Core/Util.pm +++ b/lib/MasterServer/Core/Util.pm @@ -7,18 +7,26 @@ use IP::Country::Fast; use POSIX qw/strftime/; use Exporter 'import'; -our @EXPORT = qw| valid_address ip2country |; +our @EXPORT = qw| ip2country countryflag valid_address |; ################################################################################ -## return the abbreviated country based on IP +## return the abbreviated country name based on IP ################################################################################ sub ip2country { -my ($self, $ip) = @_; + my ($self, $ip) = @_; my $reg = IP::Country::Fast->new(); return $reg->inet_atocc($ip); } ################################################################################ +## return the flag of a country +################################################################################ +sub countryflag { + my ($self, $country) = @_; + # placeholder function to return the flag of a country +} + +################################################################################ ## Verify whether a given domain name or IP address and port are valid. ## returns 1/0 if valid/invalid ip + port ################################################################################ diff --git a/lib/MasterServer/Core/Version.pm b/lib/MasterServer/Core/Version.pm index 30972e4..aba692f 100755 --- a/lib/MasterServer/Core/Version.pm +++ b/lib/MasterServer/Core/Version.pm @@ -17,22 +17,24 @@ sub version { my $self = shift; # version and author information - # -- addition to the LICENCE, you are only allowed to modify these lines - # if you send Darkelarious a postcard or email with your compliments or, - # in case of a company editing, a letter of commendation. # # You are not allowed to modify these variables without making (significant) # alterations to the source code of this master server program. Only changing # 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 or, + # in case of a company editing, a letter of (re)commendation. + # # master type - $self->{build_type} = "333networks Masterserver-Perl"; + $self->{build_type} = "333networks Masterserver-Perl (Pg-SQLite) 20150519208"; # version - $self->{build_version} = "2.0.4"; + $self->{build_version} = "2.0.8"; # date yyyy-mm-dd - $self->{build_date} = "2015-02-11"; + $self->{build_date} = "2015-05-19"; #author, email $self->{build_author} = "Darkelarious, darkelarious\@333networks.com"; diff --git a/lib/MasterServer/Database/Pg/dbClientList.pm b/lib/MasterServer/Database/Pg/dbClientList.pm new file mode 100755 index 0000000..718bf8a --- /dev/null +++ b/lib/MasterServer/Database/Pg/dbClientList.pm @@ -0,0 +1,45 @@ + +package MasterServer::Database::Pg::dbClientList; + +use strict; +use warnings; +use Exporter 'import'; + +our @EXPORT = qw| get_gamenames + get_game_list |; + + +################################################################################ +## get a list of distinct gamenames currently in the database. it does not +## matter whether they are recent or old, as long as the game is currently in +## the database. +## +## returns: hashref of gamenames +################################################################################ +sub get_gamenames { + my $self = shift; + + return $self->{dbh}->selectall_arrayref( + "SELECT distinct gamename + FROM serverlist"); +} + +################################################################################ +## get the list of games of a certain $gamename, excluding the ones excempted +## via the blacklist +## only returns server addresses that are no more than 1 hours old +################################################################################ +sub get_game_list { + my ($self, $gamename) = @_; + + return $self->{dbh}->selectall_arrayref( + "SELECT ip, port + FROM serverlist + WHERE updated > (NOW() - INTERVAL '1 HOUR') + AND gamename = ? + AND NOT blacklisted", + undef, lc $gamename); +} + + +1; diff --git a/lib/MasterServer/Database/Pg/dbCore.pm b/lib/MasterServer/Database/Pg/dbCore.pm index 8f55ebb..5cd194b 100755 --- a/lib/MasterServer/Database/Pg/dbCore.pm +++ b/lib/MasterServer/Database/Pg/dbCore.pm @@ -15,7 +15,7 @@ sub database_login { my $self = shift; # create the dbi object - my $dbh = DBI->connect(@{$self->{dblogin}}, {PrintError => 0}); + my $dbh = DBI->connect(@{$self->{dblogin}}, {PrintError => $self->{db_print}}); # verify that the database connected if (defined $dbh) { @@ -37,7 +37,7 @@ sub database_login { $self->halt(); } - # return empty element + # unreachable return undef; } diff --git a/lib/MasterServer/Database/Pg/dbServerlist.pm b/lib/MasterServer/Database/Pg/dbServerlist.pm index 832a08f..5a58717 100755 --- a/lib/MasterServer/Database/Pg/dbServerlist.pm +++ b/lib/MasterServer/Database/Pg/dbServerlist.pm @@ -7,6 +7,7 @@ use Exporter 'import'; our @EXPORT = qw| add_to_serverlist update_serverlist + syncer_add get_next_server |; ################################################################################ @@ -77,6 +78,60 @@ sub update_serverlist { return -1; } + +################################################################################ +## add new addresses to the pending list, but do not update timestamps. masters +## that sync with each other would otherwise update the timestamp for a server +## which is no longer online. +################################################################################ +sub syncer_add { + my ($self, $ip, $port, $gamename, $secure) = @_; + + # if address is in list, update the timestamp + my $u = $self->{dbh}->do( + "SELECT * FROM serverlist + WHERE ip = ? + AND port = ?", + undef, $ip, $port); + + # notify + $self->log("read","syncer found entry for $ip:$port") if ($u > 0); + + # if found, return 0 + return 0 if ($u > 0); + + # if it is already in the pending list, update it with a new challenge + $u = $self->{dbh}->do( + "UPDATE pending + SET secure = ? + WHERE ip = ? + AND heartbeat = ?", + undef, $secure, $ip, $port); + + # notify + $self->log("update","$ip:$port was updated by syncer", + $self->{log_settings}->{db_updated}) if ($u > 0); + + # return 1 if found + return 1 if ($u > 0); + + # if not found, add it + $u = $self->{dbh}->do( + "INSERT INTO pending (ip, heartbeat, gamename, secure) + SELECT ?, ?, ?, ?", + undef, $ip, $port, $gamename, $secure); + + # notify + $self->log("add","beacon: $ip:$port was added for $gamename after sync") if ($u > 0); + + # return 2 if added new + return 2 if ($u > 0); + + # or else report error + $self->log("error", "an error occurred adding $ip:$port after sync"); + return -1; +} + ################################################################################ ## get a server address of the next server in line to be queried for game info. ## query must be older than 30 seconds (in case it just got added) and not diff --git a/lib/MasterServer/Database/SQLite/dbBeacon.pm b/lib/MasterServer/Database/SQLite/dbBeacon.pm new file mode 100755 index 0000000..09eeec3 --- /dev/null +++ b/lib/MasterServer/Database/SQLite/dbBeacon.pm @@ -0,0 +1,203 @@ + +package MasterServer::Database::SQLite::dbBeacon; + +use strict; +use warnings; +use Exporter 'import'; + +our @EXPORT = qw| add_beacon + add_pending + remove_pending + get_pending_beacon + get_pending_info + get_next_pending |; + +################################################################################ +## Update beacon in serverlist or pending list. Add if beacon does not exist in +## either list. Return 0,1,2 if success in adding or -1 on error. +################################################################################ +sub add_beacon { + my ($self, $ip, $beaconport, $heartbeat, $gamename, $secure) = @_; + + # if address is in list, update the timestamp + my $u = $self->{dbh}->do( + "UPDATE serverlist + SET beacon = CURRENT_TIMESTAMP, + updated = CURRENT_TIMESTAMP, + gamename = ?, + b333ms = 1 + WHERE ip = ? + AND port = ?", + undef, lc $gamename, $ip, $heartbeat); + + # notify + $self->log("update", "beacon heartbeat for $ip:$heartbeat") if ($u > 0); + + # if serverlist was updated return 0 + return 0 if ($u > 0); + + # if it is already in the pending list, update it with a new challenge + $u = $self->{dbh}->do( + "UPDATE pending + SET added = CURRENT_TIMESTAMP, + beaconport = ?, + gamename = ?, + secure = ? + WHERE ip = ? + AND heartbeat = ?", + undef, $beaconport, lc $gamename, $secure, $ip, $heartbeat); + + # notify + $self->log("update", "beacon heartbeat $ip:$beaconport pending $gamename:$heartbeat") if ($u > 0); + + # beacon was already in pending list and was updated + return 1 if ($u > 0); + + # if not found, add it + $u = $self->{dbh}->do( + "INSERT INTO pending ( + ip, + beaconport, + heartbeat, + gamename, + secure) + SELECT ?, ?, ?, ?, ?", + undef, $ip, $beaconport, $heartbeat, lc $gamename, $secure); + + # notify + $self->log("add", "beacon heartbeat $ip:$beaconport pending $gamename:$heartbeat") if ($u > 0); + + # it was added to pending + return 2 if ($u > 0); + + # or else report error + $self->log("error", "an error occurred adding beacon $ip:$beaconport with $gamename:$heartbeat to the pending list"); + return -1; +} + +################################################################################ +## Add an address to the database that was obtained via a method other than +## an udp beacon. Return 0,1,2 if success in adding or -1 on error. +################################################################################ +sub add_pending { + my ($self, $ip, $port, $gamename, $secure) = @_; + + # if address is in list, update the timestamp + my $u = $self->{dbh}->do( + "UPDATE serverlist + SET updated = CURRENT_TIMESTAMP + WHERE ip = ? + AND port = ?", + undef, $ip, $port); + + # notify + $self->log("update", "updated serverlist with $ip:$port") if ($u > 0); + + # if updated, return 0 + return 0 if ($u > 0); + + # if it is already in the pending list, update it with a new challenge + $u = $self->{dbh}->do( + "UPDATE pending + SET added = CURRENT_TIMESTAMP, + secure = ? + WHERE ip = ? + AND heartbeat = ?", + undef, $secure, $ip, $port); + + # notify + $self->log("update", "updated pending with $ip:$port") if ($u > 0); + + # return 1 if updated + return 1 if ($u > 0); + + # if not found, add it + $u = $self->{dbh}->do( + "INSERT INTO pending ( + ip, + heartbeat, + gamename, + secure) + SELECT ?, ?, ?, ?", + undef, $ip, $port, $gamename, $secure); + + # notify + $self->log("add", "$ip:$port added pending $gamename") if ($u > 0); + + # return 2 if added new + return 2 if ($u > 0); + + # else + return -1; +} + +################################################################################ +## Remove an entry from the pending list. Returns 0 if removed or -1 in case +## of error(s). +################################################################################ +sub remove_pending { + my ($self, $id) = @_; + + # if address is in list, update the timestamp + my $u = $self->{dbh}->do( + "DELETE FROM pending + WHERE id = ?", + undef, $id); + + # notify + $self->log("delete", "removed pending id $id from pending") if ($u > 0); + + # it was removed from pending + return 2 if ($u > 0); + + # or else report error + $self->log("error", "error deleting server $id from pending"); + return -1; +} + +################################################################################ +## Get pending server by ip, beacon port. Returns * or undef +################################################################################ +sub get_pending_beacon { + my ($self, $ip, $port) = @_; + + # if address is in list, update the timestamp + return $self->{dbh}->selectall_arrayref( + "SELECT * FROM pending + WHERE ip = ? + AND beaconport = ?", + undef, $ip, $port)->[0]; +} + +################################################################################ +## Get pending server by ip, heartbeat port. Returns * or undef +################################################################################ +sub get_pending_info { + my ($self, $ip, $port) = @_; + + # if address is in list, update the timestamp + return $self->{dbh}->selectall_arrayref( + "SELECT * FROM pending + WHERE ip = ? + AND heartbeat = ?", + undef, $ip, $port)->[0]; +} + +################################################################################ +## Get server info from any entry with an id higher than the provided one. The +## server is added to pending at least 15 seconds ago. Returns info or undef. +################################################################################ +sub get_next_pending { + my ($self, $id) = @_; + + # get 1 pending id that is older than 15s + return $self->{dbh}->selectall_arrayref( + "SELECT id, ip, heartbeat, secure FROM pending + WHERE added < datetime(CURRENT_TIMESTAMP, '-15 seconds') + AND id > ? + ORDER BY id ASC LIMIT 1", + undef, $id)->[0]; +} + + +1; diff --git a/lib/MasterServer/Database/SQLite/dbClientList.pm b/lib/MasterServer/Database/SQLite/dbClientList.pm new file mode 100755 index 0000000..58c1392 --- /dev/null +++ b/lib/MasterServer/Database/SQLite/dbClientList.pm @@ -0,0 +1,45 @@ + +package MasterServer::Database::SQLite::dbClientList; + +use strict; +use warnings; +use Exporter 'import'; + +our @EXPORT = qw| get_gamenames + get_game_list |; + + +################################################################################ +## get a list of distinct gamenames currently in the database. it does not +## matter whether they are recent or old, as long as the game is currently in +## the database. +## +## returns: hashref of gamenames +################################################################################ +sub get_gamenames { + my $self = shift; + + return $self->{dbh}->selectall_arrayref( + "SELECT distinct gamename + FROM serverlist"); +} + +################################################################################ +## get the list of games of a certain $gamename, excluding the ones excempted +## via the blacklist +## only returns server addresses that are no more than 1 hours old +################################################################################ +sub get_game_list { + my ($self, $gamename) = @_; + + return $self->{dbh}->selectall_arrayref( + "SELECT ip, port + FROM serverlist + WHERE updated > datetime(CURRENT_TIMESTAMP, '-3600 seconds') + AND gamename = ? + AND NOT blacklisted", + undef, lc $gamename); +} + + +1; diff --git a/lib/MasterServer/Database/SQLite/dbCore.pm b/lib/MasterServer/Database/SQLite/dbCore.pm index a75f7a0..27c9b35 100755 --- a/lib/MasterServer/Database/SQLite/dbCore.pm +++ b/lib/MasterServer/Database/SQLite/dbCore.pm @@ -8,9 +8,8 @@ use Exporter 'import'; our @EXPORT = qw| database_login |; ################################################################################ -## database_login ## login to the database with credentials provided in the config file. -## returns dbh object +## returns dbh object or quits application on error. ################################################################################ sub database_login { my $self = shift; @@ -28,12 +27,12 @@ sub database_login { } # create the dbi object - my $dbh = DBI->connect(@{$self->{dblogin}}, {PrintError => 0}); + my $dbh = DBI->connect(@{$self->{dblogin}}, {PrintError => $self->{db_print}}); # verify that the database connected if (defined $dbh) { # log the event - $self->log("database","Connected to the SQLite database."); + $self->log("load","Connected to the SQLite database."); # turn on error printing $dbh->{printerror} = 1; @@ -44,6 +43,9 @@ sub database_login { # it takes too long to write to the database, which means that new beacons, # requests and servers cannot be processed. You don't have a choice, really.. $dbh->do("PRAGMA synchronous = OFF"); + + # allow the use of foreign keys (referencing) + $dbh->do("PRAGMA foreign_keys = ON"); # return the dbi object for further use return $dbh; diff --git a/lib/MasterServer/Database/SQLite/dbServerlist.pm b/lib/MasterServer/Database/SQLite/dbServerlist.pm new file mode 100755 index 0000000..436a788 --- /dev/null +++ b/lib/MasterServer/Database/SQLite/dbServerlist.pm @@ -0,0 +1,152 @@ + +package MasterServer::Database::SQLite::dbServerlist; + +use strict; +use warnings; +use Exporter 'import'; + +our @EXPORT = qw| add_to_serverlist + update_serverlist + syncer_add + get_next_server |; + +################################################################################ +## beacon was verified or otherwise accepted and will now be added to the +## serverlist. +################################################################################ +sub add_to_serverlist { + my ($self, $ip, $port, $gamename) = @_; + + # update or add server to serverlist + my $u = $self->{dbh}->do("UPDATE serverlist + SET updated = CURRENT_TIMESTAMP + WHERE ip = ? + AND port = ?", + undef, $ip, $port); + + # notify + $self->log("update", "$ip:$port timestamp updated") if ($u > 0); + + # if found, updated; done + return 0 if ($u > 0); + + # if not found, add it. + $u = $self->{dbh}->do("INSERT INTO serverlist (ip, port, gamename, country) + SELECT ?, ?, ?, ?", + undef, $ip, $port, $gamename, $self->ip2country($ip)); + + # notify + $self->log("add", "$ip:$port added to serverlist") if ($u > 0); + + # return added + return 1 if ($u > 0); + + # or else report error + $self->log("error", "an error occurred adding server $ip:$port ($gamename) to the serverlist"); + return -1; +} + +################################################################################ +## same as add_to_serverlist above, but does not add the server to serverlist +## if it does not exist in serverlist. it must be added by another function +## first. +################################################################################ +sub update_serverlist { + my ($self, $ip, $port, $s) = @_; + + # update server info + my $u = $self->{dbh}->do( + 'UPDATE serverlist + SET updated = CURRENT_TIMESTAMP, + gamename = ?, + gamever = ?, + hostname = ?, + hostport = ? + WHERE ip = ? + AND port = ?', undef, + $s->{gamename}, $s->{gamever}, $s->{hostname}, $s->{hostport}, + $ip, $port); + + # notify + $self->log("update", "server $ip:$port info updated") if ($u > 0); + + # return 0 if updated + return 0 if ($u > 0); + + # or else report error + $self->log("error", "an error occurred updating server $ip:$port in the serverlist"); + return -1; +} + + +################################################################################ +## add new addresses to the pending list, but do not update timestamps. masters +## that sync with each other would otherwise update the timestamp for a server +## which is no longer online. +################################################################################ +sub syncer_add { + my ($self, $ip, $port, $gamename, $secure) = @_; + + # if address is in list, update the timestamp + my $u = $self->{dbh}->do( + "SELECT * FROM serverlist + WHERE ip = ? + AND port = ?", + undef, $ip, $port); + + # notify + $self->log("read","syncer found entry for $ip:$port") if ($u > 0); + + # if found, return 0 + return 0 if ($u > 0); + + # if it is already in the pending list, update it with a new challenge + $u = $self->{dbh}->do( + "UPDATE pending + SET secure = ? + WHERE ip = ? + AND heartbeat = ?", + undef, $secure, $ip, $port); + + # notify + $self->log("update","$ip:$port was updated by syncer", + $self->{log_settings}->{db_updated}) if ($u > 0); + + # return 1 if found + return 1 if ($u > 0); + + # if not found, add it + $u = $self->{dbh}->do( + "INSERT INTO pending (ip, heartbeat, gamename, secure) + SELECT ?, ?, ?, ?", + undef, $ip, $port, $gamename, $secure); + + # notify + $self->log("add","beacon: $ip:$port was added for $gamename after sync") if ($u > 0); + + # return 2 if added new + return 2 if ($u > 0); + + # or else report error + $self->log("error", "an error occurred adding $ip:$port after sync"); + return -1; +} + +################################################################################ +## get a server address of the next server in line to be queried for game info. +## query must be older than 30 seconds (in case it just got added) and not +## older than 3 hours. +################################################################################ +sub get_next_server { + my ($self, $id) = @_; + + return $self->{dbh}->selectall_arrayref( + "SELECT id, ip, port FROM serverlist + WHERE added < datetime(CURRENT_TIMESTAMP, '-15 seconds') + AND updated > datetime(CURRENT_TIMESTAMP, '-10800 seconds') + AND id > ? + AND NOT blacklisted + ORDER BY id ASC LIMIT 1", undef, $id)->[0]; +} + +1; diff --git a/lib/MasterServer/TCP/BrowserHost.pm b/lib/MasterServer/TCP/BrowserHost.pm new file mode 100755 index 0000000..b30b27e --- /dev/null +++ b/lib/MasterServer/TCP/BrowserHost.pm @@ -0,0 +1,68 @@ + +package MasterServer::TCP::BrowserHost; + +use strict; +use warnings; +use AnyEvent::Socket; +use AnyEvent::Handle; +use Exporter 'import'; + +our @EXPORT = qw| browser_host clean_tcp_handle|; + +################################################################################ +## wait for incoming TCP connections from game clients and other masterservers. +## respond with secure/validate, contact info and/or server lists. +## allow other masterservers to synchronize +################################################################################ +sub browser_host { + my $self = shift; + + # log: TCP host is active + $self->log("load","Loading TCP Browser Host."); + + my $browser = tcp_server undef, $self->{listen_port}, sub { + my ($fh, $a, $p) = @_; + + # validated? yes = 1 no = 0 + my $auth = 0; + + # debug -- new connection opened + $self->log("tcp","New connection from $a:$p"); + + # prep a challenge + my $secure = $self->secure_string(); + + # handle received data + my $h; $h = AnyEvent::Handle->new( + fh => $fh, + poll => 'r', + timeout => 1, + on_eof => sub {$self->clean_tcp_handle(@_)}, + on_error => sub {$self->clean_tcp_handle(@_)}, + on_read => sub {$self->read_tcp_handle($h, $a, $p, $secure, @_)}, + ); + + # part 1: send \basic\\secure\$key\ + $h->push_write("\\basic\\\\secure\\$secure\\final\\"); + + # keep handle alive longer and store authentication info + $self->{browser_clients}->{$h} = [$h, $auth]; + return; + }; + + # startup of TCP server complete + $self->log("load", "Listening for TCP connections on port $self->{listen_port}."); + return $browser; +} + +################################################################################ +## clean handles on timeouts, completed requests and/or errors +################################################################################ +sub clean_tcp_handle{ + my ($self, $c) = @_; + # clean and close the connection + delete ($self->{browser_clients}->{$c}); + $c->destroy(); +} + +1; diff --git a/lib/MasterServer/TCP/Handler.pm b/lib/MasterServer/TCP/Handler.pm new file mode 100755 index 0000000..bf6beb9 --- /dev/null +++ b/lib/MasterServer/TCP/Handler.pm @@ -0,0 +1,280 @@ + +package MasterServer::TCP::Handler; + +use strict; +use warnings; +use AnyEvent::Socket; +use AnyEvent::Handle; +use Exporter 'import'; + +our @EXPORT = qw| read_tcp_handle + handle_validate + handle_about + handle_list + handle_sync |; + +################################################################################ +## wait for incoming TCP connections from game clients and other masterservers. +## respond with secure/validate, contact info and/or server lists. +## allow other masterservers to synchronize +################################################################################ +sub read_tcp_handle { + my ($self, $h, $a, $p, $secure, $c) = @_; + + # clear the buffer + my $m = $c->rbuf; + $c->rbuf = ""; + + # did the client validate already? + my $val = $self->{browser_clients}->{$h}[1]; + + # in case of errors, save the original message + my $rxbuf = $m; + + # allow multiple blocks to add to the response string + my $response = ""; + + # replace empty values for the string "undef" and replace line endings from netcatters + # parse the received data and extrapolate all the query commands found + my %r = (); + $m =~ s/\\\\/\\undef\\/; + $m =~ s/\n//; + $m =~ s/\\([^\\]+)\\([^\\]+)/$r{$1}=$2/eg; + + # secure/validate challenge + # part 2: receive \gamename\ut\location\0\validate\$validate\final\ + $val = $self->handle_validate(\%r, $h, $secure, $a, $p) + if (exists $r{validate} && !$val); + + # about query + $response .= $self->handle_about($r{about}, $a, $p) if (exists $r{about}); + + # return address list + # part 3: wait for the requested action: \list\gamename\ut\ + $self->handle_list($val, \%r, $c, $a, $p) if (exists $r{list} && exists $r{gamename}); + + # Sync request from another 333networks-based masterserver. Respond with list + # of requested games (or all games). + $self->handle_sync($val, \%r, $c, $a, $p) if (exists $r{sync}); + + # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + # improper syntax/protocol -- no valid commands found + # respond with an error. + # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + if ($m =~ m/!(about|sync|validate|list)/) { + + # error message to client + $c->push_write("\\echo\\333networks did not understand your request. ". + "Contact us via 333networks.com\\final\\"); + + # and log it + $self->log("error","invalid request from Browser $a:$p with unknown message \"$rxbuf\"", $self->{log_settings}->{handler_error}); + } # end if weird query + else { + $c->push_write($response . "\\final\\") if ($response ne ""); + } + +} + + +################################################################################ +## The master server opens the connection with the \secure\ challenge. The +## client should respond with basic information about itself and the +## \validate\ response. In this code block we verify the challenge/response. +################################################################################ +sub handle_validate { + my ($self, $r, $h, $secure, $a, $p) = @_; + + # auth var init + my $val = 0; + + # pass or fail the secure challenge + if (exists $r->{gamename} && exists $self->{game}->{$r->{gamename}}) { + # game exists and we have the key to verify the response + $val = $self->validated_request($r->{gamename}, $secure, $r->{enctype}, $r->{validate}); + + # update for future queries + $self->{browser_clients}->{$h}[1] = $val; + } + elsif (exists $r->{gamename}) { + # log + $self->log("support", "received unknown gamename request \"$r->{gamename}\" from $a:$p"); + } + + # log + $self->log("secure","$a:$p validated with $val for $r->{gamename}, $secure, $r->{validate}"); + + # return auth status + return $val; +} + + +################################################################################ +## \about\ query. +## Can contain the values "contact", "build", "Address" or "support", where +## - contact: return contact information; see config.pl +## - build: build info, also version.pm +## - address: address and ports +## - support: return a list of games currently in the database +## - undef: all of the above +## +## NOTE: client does not need to validate to be allowed to perform this +## query. +################################################################################ +sub handle_about { + my ($self, $about, $a, $p) = @_; + my $response = ""; + + # + # contact info + # + if ($about =~ /^contact$/i or $about =~ /^undef$/i) { + $response .= "\\about\\$self->{contact_details}"; + + # log/print + $self->log("about","communicating to $a:$p my contact information."); + } + + # + # build info + # + if ($about =~ /^build$/i or $about =~ /^undef$/i) { + + $response .= "\\build\\$self->{build_type} $self->{build_version} written " + . "by $self->{build_author}, released $self->{build_date}"; + + # log/print + $self->log("about","telling $a:$p my build info."); + } + + # + # address info + # + if ($about =~ /^address$/i or $about =~ /^undef$/i) { + + $response .= "\\address\\$self->{masterserver_address}" + . "\\listen_port\\$self->{listen_port}" + . "\\beacon_port\\$self->{beacon_port}"; + + # log/print + $self->log("about","telling $a:$p my address/config info."); + } + + # + # support info + # + if ($about =~ /^support$/i or $about =~ /^undef$/i) { + + # string games in database + my $sg = $self->get_gamenames(); + my $sgs = ""; + for (@{$sg}) { + $sgs .= " " if (length $sgs > 0); + $sgs .= $_->[0]; + } + + # print response + $response .= "\\support\\$sgs"; + + #log/print + $self->log("about","telling $a:$p which games are supported."); + } + + # return response string + return $response; +} + +################################################################################ +## At this point, the client should be validated and ready to request with +## the \secure\ command and is allowed to ask for the list. +## The \list\ cmd is NOT compatible with combined queries +## (like \about\contact\list\) +################################################################################ +sub handle_list { + my ($self, $val, $r, $c, $a, $p) = @_; + + # confirm validation + if ($val && exists $r->{gamename}) { + + # prepare the list + my $data = ""; + + # determine the return format + if ($self->{hex_format} =~ m/$r->{gamename}/i or $r->{gamename} =~ /^cmp$/i) { + # return addresses as byte format (ip=ABCD port=EF) + $data .= $self->compile_list_cmp($r->{gamename}); + } + else { + # return addresses as regular \ip\127.0.0.1:7777\ format + $data .= $self->compile_list($r->{gamename}); + } + + # finalize response string + $data .= "\\final\\"; + + # immediately send to client + $c->push_write($data); + + # log successful (debug) + $self->log("list","$a:$p successfully retrieved the list for $r->{gamename}."); + + # clean and close the connection + $self->clean_tcp_handle($c); + } + + # proper syntax/protocol, but incorrect validation. Therefore respond with + # an 'empty' list, returning only \final\. + else { + # return error/empty list + $c->push_write("\\echo\\333networks failed to validate your request. Use the correct authorization cipher!\\final\\"); + + # log it too + $self->log("error","browser $a:$p failed validation for $r->{gamename}"); + + # clean and close the connection + $self->clean_tcp_handle($c); + } +} + +################################################################################ +## Respond to \sync\ requests from other 333networks-based masterservers. After +## validation, sync behaves in much the same way as \list\, +################################################################################ +sub handle_sync { + my ($self, $val, $r, $c, $a, $p) = @_; + + # alternate part 3: wait for the requested action: \sync\(all|list of games) + $self->log("tcp","Sync request from $a:$p found"); + + if ($val && exists $r->{sync}) { + + # compile list of addresses + my $data = $self->compile_sync($r->{sync}); + $data .= "\\final\\"; + + # send to remote client + $c->push_write($data); + + # log successful (debug) + $self->log("sync","$a:$p successfully synced."); + + # clean and close the connection + $self->clean_tcp_handle($c); + + } + # proper syntax/protocol, but incorrect validation. Therefore respond with + # an 'empty' list, returning only \final\. + else { + + # return error/empty list + $c->push_write("\\echo\\333networks failed to validate your request. Use a proper authorization key!\\final\\"); + + # log it too + $self->log("error","$a:$p failed synchronization."); + + # clean and close the connection + $self->clean_tcp_handle($c); + } +} + +1; diff --git a/lib/MasterServer/TCP/ListCompiler.pm b/lib/MasterServer/TCP/ListCompiler.pm new file mode 100755 index 0000000..56bb256 --- /dev/null +++ b/lib/MasterServer/TCP/ListCompiler.pm @@ -0,0 +1,114 @@ + +package MasterServer::TCP::ListCompiler; + +use strict; +use warnings; +use Exporter 'import'; + +our @EXPORT = qw| compile_list compile_list_cmp compile_sync |; + +################################################################################ +## compile the list of \ip\ip:port\ addresses and parse them into the +## plaintext return string. +################################################################################ +sub compile_list { + my ($self, $gamename) = @_; + + # get the list from database + my $serverlist = $self->get_game_list($gamename); + + # prepare empty return string + my $response_string = ""; + + # add address as regular \ip\127.0.0.1:7777\ format + for (@{$serverlist}){ + + # append \ip\ip:port to string + $response_string .= "\\ip\\$_->[0]:$_->[1]"; + } + + # return the string with data + return $response_string; +} + +################################################################################ +## compile the list of binary ip:port addresses and parse them into the +## ABCDE return string. +################################################################################ +sub compile_list_cmp { + my ($self, $gamename) = @_; + + # get the list from database + my $serverlist = $self->get_game_list($gamename); + + # prepare empty return string + my $response_string = ""; + + # compile a return string + for (@{$serverlist}){ + + # convert ip address to ABCDEF mode + my ($A, $B, $C, $D) = ($_->[0] =~ /(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/); + my ($E, $F) = ($_->[1] >> 8, $_->[1] & 0xFF); + + # print as chr string of 6 bytes long + my $bin = ""; $bin .= (chr $A) . (chr $B) . (chr $C) . (chr $D) . (chr $E) . (chr $F); + + # append to list of addresses + $response_string .= $bin; + } + + # return the string with data + return $response_string; +} + + +################################################################################ +## compile a list of all requested games --or-- if not specified, a list of +## all games +################################################################################ +sub compile_sync { + my ($self, $sync) = @_; + + # prepare empty return string + my $response_string = ""; + my @games; + + # client requests to sync all games + if ($sync eq "all") { + # get array of gamenames from db + my $sg = $self->get_gamenames(); + for (@{$sg}) {push @games, $_->[0];} + } + # only selected games + else { + # split request into array + @games = split " ", $sync; + } + + # only get unique values from array + @games = map { $_ => 1 } @games; + + # get the list for every requested gamename + for my $g (@games) { + + # $g is now a gamename -- check if it's supported. Else ignore. + if (exists $self->{game}->{$g}) { + + # get list from database + my $list = $self->get_game_list($g); + + # add all games to string separated by spaces + my $gamestring = ""; + foreach $_ (@{$list}) {$gamestring .= "$_->[0]:$_->[1] ";} + + # if it contains at least one entry, add the list to the response list + $response_string .= "\\$g\\$gamestring" if (length $gamestring >= 7); + } + } + + # return \gamename\addresses\gamename2\addresses2 list + return $response_string; +} + +1; diff --git a/lib/MasterServer/TCP/Syncer.pm b/lib/MasterServer/TCP/Syncer.pm new file mode 100755 index 0000000..dea20a5 --- /dev/null +++ b/lib/MasterServer/TCP/Syncer.pm @@ -0,0 +1,166 @@ + +package MasterServer::TCP::Syncer; + +use strict; +use warnings; +use AnyEvent; +use AnyEvent::Handle; +use Exporter 'import'; +use Data::Dumper 'Dumper'; + +our @EXPORT = qw| syncer_scheduler sync_with_master process_sync_list|; + +################################################################################ +## Syncer Scheduler +## Request the masterlist for selected or all games from other +## 333networks-based masterservers. +################################################################################ +sub syncer_scheduler { + my $self = shift; + + # log active + $self->log("load", "Synchronisation module active."); + + # go through the list of provided addresses + my $i = 0; + return AnyEvent->timer ( + after => $self->{sync_time}[0], + interval => $self->{sync_time}[1], + cb => sub { + # check if there's a master server entry to be synced. If not, return + # to zero and go all over again. + $i = 0 unless $self->{sync_masters}[$i]; + return if (!defined $self->{sync_masters}[$i]); + + # synchronze with master $i + $self->log("tcp", "Attempting to synchronize with $self->{sync_masters}[$i]->{address}"); + $self->sync_with_master($self->{sync_masters}[$i]); + + #increment counter + $i++; + } + ); +} + +################################################################################ +## Sends synchronization request to another 333networks based master server and +## receives the list of games. +################################################################################ +sub sync_with_master { + my ($self, $ms) = @_; + + # list to store all IPs in. + my $sync_list = ""; + + # connection handle + my $handle; + $handle = new AnyEvent::Handle( + connect => [$ms->{address} => $ms->{port}], + timeout => 3, + poll => 'r', + on_error => sub {$self->log("error","$! on $ms->{address} $ms->{port}"); $handle->destroy;}, + on_eof => sub {$self->process_sync_list($sync_list, $ms); $handle->destroy;}, + on_read => sub { + # receive and clear buffer + my $m = $_[0]->rbuf; + $_[0]->rbuf = ""; + + # remove string terminator: sometimes trailing slashes are added or + # forgotten by sender, so \secure\abcdef is actually \secure\abcdef{\0} + chop $m if $m =~ m/secure/; + + # part 1: receive \basic\\secure\$key + if ($m =~ m/basic\\\\secure/) { + + # hash $m into %r + my %r = (); + $m =~ s/\\\\/\\undef\\/; + $m =~ s/\n//; + $m =~ s/\\([^\\]+)\\([^\\]+)/$r{$1}=$2/eg; + + # respond to the validate challenge + my $validate = $self->validate_string("333networks", $r{secure}, $r{enctype}); + + # part 2: send \gamename\ut\location\0\validate\$validate\final\ + $handle->push_write("\\gamename\\333networks\\location\\0\\validate\\$validate\\final\\"); + + # part 3: request the list \sync\gamenames consisting of space-seperated game names or "all" + my $request = "\\sync\\".(($self->{sync_games}[0] == 0) ? "all" : $self->{sync_games}[1])."\\final\\"; + + # push the request to remote host + $handle->push_write($request); + + # clean up $m for future receivings + $m = ""; + + } # end secure + + # part 4: receive the entire list in multiple steps + $sync_list .= $m; + }, + ); +} + +################################################################################ +## Process the list of addresses that was received after querying the UCC applet +## and store them in the pending list. +################################################################################ +sub process_sync_list { + my ($self, $m, $ms) = @_; + + # replace empty values for the string "undef" and replace line endings from netcatters + # parse hash {gamename => list of ips seperated by space} + my %r = (); + $m =~ s/\\\\/\\undef\\/; + $m =~ s/\n//; + $m =~ s/\\([^\\]+)\\([^\\]+)/$r{$1}=$2/eg; + + # counter + my $c = 0; + + # iterate through the gamenames and addresses + while ( my ($gn,$addr) = each %r) { + + # only process gamenames that are in our list for supported games (supportedgames.pl) + if (defined $gn && exists $self->{game}->{lc $gn}) { + + # database types such as SQLite are slow, therefore use transactions. + $self->{dbh}->begin_work; + + # l(ocations, \label\ip:port\) split up in a(ddress) and p(ort) + foreach my $l (split(/ /, $addr)) { + + # search for \255.255.255.255:7778\, contains ':' + if ($l =~ /:/) { + my ($a,$p) = $l =~ /(.*):(.*)/; + + # check if address entry is valid + if ($self->valid_address($a,$p)) { + # count number of valid addresses + $c++; + + # add server + $self->syncer_add($a, $p, $gn, $self->secure_string()); + + # print address + $self->log("add", "syncer added $gn\t$a\t$p"); + } + else { + # invalid address, log + $self->log("error", "invalid address found while syncing at $ms->{address}: $l!"); + } + + } # endif ($l =~ /:/) + } # end for / / + + # end transaction, commit + $self->{dbh}->commit; + + } # end defined $gn + } # end while + + # end message + $self->log("sync", "received $c addresses after syncing from $ms->{address}"); +} + +1; diff --git a/lib/MasterServer/UDP/BeaconCatcher.pm b/lib/MasterServer/UDP/BeaconCatcher.pm index 685c33b..0d5ce81 100755 --- a/lib/MasterServer/UDP/BeaconCatcher.pm +++ b/lib/MasterServer/UDP/BeaconCatcher.pm @@ -38,7 +38,7 @@ sub beacon_catcher { } ################################################################################ -## Determine the concent of the received information and process it. +## Determine the content of the received information and process it. ################################################################################ sub on_beacon_receive { # $self, beacon address, handle, packed client address diff --git a/lib/MasterServer/UDP/BeaconChecker.pm b/lib/MasterServer/UDP/BeaconChecker.pm index 98ee1e2..a99be72 100755 --- a/lib/MasterServer/UDP/BeaconChecker.pm +++ b/lib/MasterServer/UDP/BeaconChecker.pm @@ -80,10 +80,9 @@ sub beacon_checker { # 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. + # From here on, just count down until the cycle is complete and handle + # new entries while they are added to the list. - # debug (spams badly) - $self->log("debug_spam", "Checker timer: t=".(time - $q{start_time})); } ); @@ -105,7 +104,7 @@ sub query_udp_server { my $buf = ""; # debug spamming - $self->log("debug_spam", "Query server $ip:$port"); + $self->log("udp", "Query server $ip:$port"); # connect with UDP server my $udp_client; $udp_client = AnyEvent::Handle::UDP->new( diff --git a/lib/MasterServer/UDP/BeaconProcessor.pm b/lib/MasterServer/UDP/BeaconProcessor.pm deleted file mode 100755 index a41905f..0000000 --- a/lib/MasterServer/UDP/BeaconProcessor.pm +++ /dev/null @@ -1,120 +0,0 @@ - -package MasterServer::UDP::BeaconProcessor; - -use strict; -use warnings; -use Data::Dumper 'Dumper'; -use AnyEvent::Handle::UDP; -use Exporter 'import'; - -our @EXPORT = qw| process_udp_beacon process_udp_validate |; - - -## process beacons that have a \heartbeat\ and \gamename\ format -sub process_udp_beacon { - # $self, handle, packed address, udp data, peer ip address, $port - my ($self, $udp, $pa, $buf, $peer_addr, $port) = @_; - - # received heartbeat in $buf: \heartbeat\7778\gamename\ut\ - my %r; - $buf =~ s/\\([^\\]+)\\([^\\]+)/$r{$1}=$2/eg; - - # check whether the beacon has a gamename that is supported in our list - if (defined $r{gamename} && exists $self->{game}->{lc $r{gamename}}) { - # log the 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. - if ($r{heartbeat} == 0) { - # assuming a default port exists - if (exists $self->{game}->{lc $r{gamename}}->{port}) { - $r{heartbeat} = $self->{game}->{lc $r{gamename}}->{port}; - } - } - - # - # verify valid server address (ip+port) - if ($self->valid_address($peer_addr,$r{heartbeat})) { - - # 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); - - # send secure string back - if ($auth > 0) { - - # verify that this is a legitimate client by sending the "secure" query - $udp->push_send("\\secure\\$secure\\final\\", $pa); - - # log this as a new beacon - $self->log("secure", "challenged new beacon $peer_addr:$port with $secure."); - } - } - - # 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"); - } - } - - # gamename not valid or not found in supportedgames.pl - else { - # log - $self->log("support", "received unknown beacon \"$r{gamename}\" from $peer_addr:$r{heartbeat}"); - } -} - - -## process the received validate query and determine whether the server is allowed in our database -sub process_udp_validate { - # $self, udp data, ip, port - my ($self, $buf, $peer_addr, $port, $heartbeat) = @_; - - # received heartbeat in $b: \validate\string\queryid\99.9\ - my %r; - $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); - - # if indeed in the pending list, check - 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} && exists $self->{game}->{lc $r{gamename}}) ? $r{gamename} : $pending->[4]; - - # verify challenge gamename secure enctype validate_response - my $val = $self->validated_beacon($pending->[4], $pending->[5], $enc, $r{validate}); - - # log challenge results - $self->log("secure", "$peer_addr:$port validated with $val for $pending->[4]"); - - # if validated, add to db - if ($val > 0) { - - # successfully added? ip, query port, gamename - my $sa = $self->add_to_serverlist($pending->[1], $pending->[3], $pending->[4]); - - # remove the entry from pending if successfully added - $self->remove_pending($pending->[0]) if ( $sa >= 0); - - # and set as direct beacon - $self->set_direct_beacon($pending->[1], $pending->[3]); - } - else { - # else failed validation - $self->log("error","beacon $peer_addr:$port failed validation for $pending->[4] (details: $pending->[5] sent, got $r{validate})"); - } - } -} - -1; diff --git a/lib/MasterServer/UDP/DatagramProcessor.pm b/lib/MasterServer/UDP/DatagramProcessor.pm index c658264..632435c 100755 --- a/lib/MasterServer/UDP/DatagramProcessor.pm +++ b/lib/MasterServer/UDP/DatagramProcessor.pm @@ -208,7 +208,7 @@ sub process_ucc_applet_query { $c++; # print address - $self->log("debug_spam", "applet query added $ms->{game}\t$a\t$p"); + $self->log("add", "applet query added $ms->{game}\t$a\t$p"); # add server $self->add_pending($a, $p, $ms->{game}, $self->secure_string()); diff --git a/lib/MasterServer/UDP/UCCAppletQuery.pm b/lib/MasterServer/UDP/UCCAppletQuery.pm index bd7bb73..f92c20c 100755 --- a/lib/MasterServer/UDP/UCCAppletQuery.pm +++ b/lib/MasterServer/UDP/UCCAppletQuery.pm @@ -10,7 +10,8 @@ use Exporter 'import'; our @EXPORT = qw| ucc_applet_query_scheduler query_applet |; ################################################################################ -## Query other UCC applets periodically to get a list of online servers. +## 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; @@ -44,7 +45,7 @@ sub query_applet { my ($self, $ms) = @_; # be nice to notify - $self->log("applet","[TCP] > start querying $ms->{ip}:$ms->{port} for '$ms->{game}' games"); + $self->log("query","start querying $ms->{ip}:$ms->{port} for '$ms->{game}' games"); # list to store all IPs in. my $master_list = ""; @@ -68,7 +69,7 @@ sub query_applet { # part 1: receive \basic\\secure\$key if ($m =~ m/\\basic\\\\secure\\/) { - # skip to part 3: also request the list \list\gamename\ut + # 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\\"); } diff --git a/log/logfile.log b/log/logfile.log new file mode 100644 index 0000000..00eb799 --- /dev/null +++ b/log/logfile.log @@ -0,0 +1 @@ +Logfiles are stored in this folder by default. You can delete this file. diff --git a/util/masterserver.pl b/util/masterserver.pl index 9142d82..078eba1 100755 --- a/util/masterserver.pl +++ b/util/masterserver.pl @@ -19,7 +19,9 @@ require "$ROOT/data/masterserver-config.pl"; $MasterServer::OBJ->{$_} = $S{$_} for (keys %S); # load MasterServer core libs -MasterServer::load_recursive('MasterServer::Core', 'MasterServer::UDP'); +MasterServer::load_recursive('MasterServer::Core', + 'MasterServer::UDP', + 'MasterServer::TCP'); # Run the MasterServer process MasterServer::run(); |
