diff options
Diffstat (limited to 'lib')
20 files changed, 1179 insertions, 152 deletions
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\\"); } |
