diff options
Diffstat (limited to 'lib')
| -rwxr-xr-x | lib/MasterServer/Core/Secure.pm | 40 | ||||
| -rwxr-xr-x | lib/MasterServer/Core/Version.pm | 4 | ||||
| -rwxr-xr-x | lib/MasterServer/Database/Pg/dbCiphers.pm | 54 | ||||
| -rwxr-xr-x | lib/MasterServer/Database/Pg/dbGetServers.pm | 16 | ||||
| -rwxr-xr-x | lib/MasterServer/Database/SQLite/dbCiphers.pm | 54 | ||||
| -rwxr-xr-x | lib/MasterServer/Database/SQLite/dbGetServers.pm | 16 | ||||
| -rwxr-xr-x | lib/MasterServer/TCP/Handler.pm | 72 | ||||
| -rwxr-xr-x | lib/MasterServer/TCP/ListCompiler.pm | 2 | ||||
| -rwxr-xr-x | lib/MasterServer/UDP/BeaconCatcher.pm | 9 | ||||
| -rwxr-xr-x | lib/MasterServer/UDP/DatagramProcessor.pm | 8 | ||||
| -rwxr-xr-x | lib/MasterServer/UDP/UpLink.pm | 140 |
11 files changed, 215 insertions, 200 deletions
diff --git a/lib/MasterServer/Core/Secure.pm b/lib/MasterServer/Core/Secure.pm index 6d05f82..125e276 100755 --- a/lib/MasterServer/Core/Secure.pm +++ b/lib/MasterServer/Core/Secure.pm @@ -39,7 +39,6 @@ sub load_ciphers { # insert the game/cipher in the db or halt on error if ($self->insert_cipher(%opt) < 0) { - # failure causes a fatal error and exits $self->{dbh}->rollback; $self->halt(); } @@ -52,15 +51,12 @@ sub load_ciphers { ################################################################################ # generate a random string of 6 characters long for the \secure\ challenge -# returns string +# returns a random string, only uppercase characters ################################################################################ sub secure_string { - # generate a random string, only uppercase characters my @c = ('A'..'Z'); my $s = ""; $s .= $c[rand @c] for 1..6; - - # return random string return $s; } @@ -73,28 +69,22 @@ sub compare_challenge { # debugging enabled? Then don't care about validation return 1 if ($self->{debug_validate}); - - # secure string too long? (because vulnerable in UE) - return 0 if (length $o{secure} > 16); - + # ignore this game if asked to do so if ($self->{ignore_browser_key} =~ m/$o{gamename}/i){ $self->log("ignore", "ignored beacon validation for $o{gamename}"); return 1; } - # enctype given? - $o{enctype} = 0 unless $o{enctype}; - # calculate validate string my $val = get_validate_string( - $self->get_game_props($o{gamename})->{cipher}, + $self->get_game_props(gamename => $o{gamename})->[0]->{cipher}, $o{secure}, - $o{enctype} + $o{enctype} || 0 ); - # return whether or not they match - return ($val eq $o{validate}); + # return match or no match + return ($val eq ($o{validate} || "")); } ################################################################################ @@ -103,17 +93,15 @@ sub compare_challenge { sub validate_string { my ($self, %o) = @_; - # secure string too long? (because vulnerable in UE) - return 0 if (length $o{secure} > 16); - - # get cipher from gamename - my $cip = $self->get_game_props(lc $o{gamename})->{cipher}; - - # enctype given? - $o{enctype} = 0 unless $o{enctype}; - + # secure string too long? discard as hack. + return 0 if (length $o{secure} > 6); + # calculate and return validate string - return get_validate_string($cip, $o{secure}, $o{enctype}); + return get_validate_string( + $self->get_game_props(gamename => $o{gamename})->[0]->{cipher}, + $o{secure}, + $o{enctype} || 0 + ); } ################################################################################ diff --git a/lib/MasterServer/Core/Version.pm b/lib/MasterServer/Core/Version.pm index f87ea8d..4a49392 100755 --- a/lib/MasterServer/Core/Version.pm +++ b/lib/MasterServer/Core/Version.pm @@ -29,13 +29,13 @@ sub version { $self->{build_type} = "333networks Masterserver-Perl Multidb"; # version - $self->{build_version} = "2.3.0"; + $self->{build_version} = "2.3.1"; # short version for uplinks $self->{short_version} = "MS-perl $self->{build_version}"; # date yyyy-mm-dd - $self->{build_date} = "2017-05-13"; + $self->{build_date} = "2017-07-06"; #author, email $self->{build_author} = "Darkelarious, darkelarious\@333networks.com"; diff --git a/lib/MasterServer/Database/Pg/dbCiphers.pm b/lib/MasterServer/Database/Pg/dbCiphers.pm index e099f88..5343065 100755 --- a/lib/MasterServer/Database/Pg/dbCiphers.pm +++ b/lib/MasterServer/Database/Pg/dbCiphers.pm @@ -7,7 +7,8 @@ use Exporter 'import'; our @EXPORT = qw| check_cipher_count clear_ciphers insert_cipher - get_game_props |; + get_game_props + get_gamenames |; ################################################################################ ## Check if ciphers exist @@ -51,13 +52,56 @@ sub insert_cipher { ################################################################################ ## get the cipher, description and default port that goes with given gamename -## returns only the first item if multiple items +## ################################################################################ sub get_game_props { - my ($self, $gn) = @_; + my $s = shift; + my %o = ( + sort => '', + @_ + ); - # get cipher from db if gamename exists - return $self->db_all('SELECT * FROM games WHERE gamename = ?', lc $gn)->[0]; + my %where = ( + $o{gamename} ? ('gamename = ?' => lc $o{gamename}) : (), + $o{cipher} ? ('cipher = ?' => $o{cipher}) : (), + $o{description} ? ('description = ?' => $o{description}) : (), + $o{default_qport} ? ('default_qport = ?' => $o{default_qport}) : (), + $o{num_uplink} ? ('num_uplink = ?' => $o{num_uplink}) : (), + $o{num_total} ? ('num_total = ?' => $o{num_total}) : (), + $o{num_gt} ? ('num_total >= ?' => $o{num_gt}) : (), + ); + + my @select = ( qw| gamename cipher description default_qport num_uplink num_total|,); + my $order = sprintf { + gamename => 'gamename %s', + cipher => 'cipher %s', + description => 'description %s', + default_qport => 'default_qport %s', + num_uplink => 'num_uplink %s', + num_total => 'num_total %s', + }->{ $o{sort}||'gamename' }, $o{reverse} ? 'DESC' : 'ASC'; + + return $s->db_all( q| + SELECT !s FROM games + !W + ORDER BY !s| + .($o{limit} ? " LIMIT ?" : ""), + join(', ', @select), \%where, $order, ($o{limit} ? $o{limit} : ()), + ); +} + + +################################################################################ +## 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. +################################################################################ +sub get_gamenames { + my $self = shift; + + return $self->{dbh}->selectall_arrayref( + "SELECT distinct gamename + FROM serverlist"); } 1; diff --git a/lib/MasterServer/Database/Pg/dbGetServers.pm b/lib/MasterServer/Database/Pg/dbGetServers.pm index a33582a..e9bfaec 100755 --- a/lib/MasterServer/Database/Pg/dbGetServers.pm +++ b/lib/MasterServer/Database/Pg/dbGetServers.pm @@ -5,8 +5,7 @@ use warnings; use Exporter 'import'; our @EXPORT = qw| get_server - get_pending - get_gamenames |; + get_pending |; ################################################################################ ## get server details for one or multiple servers @@ -123,17 +122,4 @@ sub get_pending { ); } -################################################################################ -## 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. -################################################################################ -sub get_gamenames { - my $self = shift; - - return $self->{dbh}->selectall_arrayref( - "SELECT distinct gamename - FROM serverlist"); -} - 1; diff --git a/lib/MasterServer/Database/SQLite/dbCiphers.pm b/lib/MasterServer/Database/SQLite/dbCiphers.pm index 5b88944..f257b7b 100755 --- a/lib/MasterServer/Database/SQLite/dbCiphers.pm +++ b/lib/MasterServer/Database/SQLite/dbCiphers.pm @@ -7,7 +7,8 @@ use Exporter 'import'; our @EXPORT = qw| check_cipher_count clear_ciphers insert_cipher - get_game_props |; + get_game_props + get_gamenames |; ################################################################################ ## Check if ciphers exist @@ -51,13 +52,56 @@ sub insert_cipher { ################################################################################ ## get the cipher, description and default port that goes with given gamename -## returns only the first item if multiple items +## ################################################################################ sub get_game_props { - my ($self, $gn) = @_; + my $s = shift; + my %o = ( + sort => '', + @_ + ); - # get cipher from db if gamename exists - return $self->db_all('SELECT * FROM games WHERE gamename = ?', lc $gn)->[0]; + my %where = ( + $o{gamename} ? ('gamename = ?' => lc $o{gamename}) : (), + $o{cipher} ? ('cipher = ?' => $o{cipher}) : (), + $o{description} ? ('description = ?' => $o{description}) : (), + $o{default_qport} ? ('default_qport = ?' => $o{default_qport}) : (), + $o{num_uplink} ? ('num_uplink = ?' => $o{num_uplink}) : (), + $o{num_total} ? ('num_total = ?' => $o{num_total}) : (), + $o{num_gt} ? ('num_total >= ?' => $o{num_gt}) : (), + ); + + my @select = ( qw| gamename cipher description default_qport num_uplink num_total|,); + my $order = sprintf { + gamename => 'gamename %s', + cipher => 'cipher %s', + description => 'description %s', + default_qport => 'default_qport %s', + num_uplink => 'num_uplink %s', + num_total => 'num_total %s', + }->{ $o{sort}||'gamename' }, $o{reverse} ? 'DESC' : 'ASC'; + + return $s->db_all( q| + SELECT !s FROM games + !W + ORDER BY !s| + .($o{limit} ? " LIMIT ?" : ""), + join(', ', @select), \%where, $order, ($o{limit} ? $o{limit} : ()), + ); +} + + +################################################################################ +## 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. +################################################################################ +sub get_gamenames { + my $self = shift; + + return $self->{dbh}->selectall_arrayref( + "SELECT distinct gamename + FROM serverlist"); } 1; diff --git a/lib/MasterServer/Database/SQLite/dbGetServers.pm b/lib/MasterServer/Database/SQLite/dbGetServers.pm index 1884601..719e00a 100755 --- a/lib/MasterServer/Database/SQLite/dbGetServers.pm +++ b/lib/MasterServer/Database/SQLite/dbGetServers.pm @@ -5,8 +5,7 @@ use warnings; use Exporter 'import'; our @EXPORT = qw| get_server - get_pending - get_gamenames |; + get_pending |; ################################################################################ ## get server details for one or multiple servers @@ -123,17 +122,4 @@ sub get_pending { ); } -################################################################################ -## 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. -################################################################################ -sub get_gamenames { - my $self = shift; - - return $self->{dbh}->selectall_arrayref( - "SELECT distinct gamename - FROM serverlist"); -} - 1; diff --git a/lib/MasterServer/TCP/Handler.pm b/lib/MasterServer/TCP/Handler.pm index 4e174c2..eb0094d 100755 --- a/lib/MasterServer/TCP/Handler.pm +++ b/lib/MasterServer/TCP/Handler.pm @@ -8,7 +8,6 @@ use Exporter 'import'; our @EXPORT = qw| read_tcp_handle handle_validate - handle_about handle_list handle_sync |; @@ -45,9 +44,6 @@ sub read_tcp_handle { # 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\ @@ -65,7 +61,7 @@ sub read_tcp_handle { # improper syntax/protocol -- no valid commands found # respond with an error. # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - if ("about sync validate list" =~ m/\Q$response\E/i) { + if ("sync validate list" =~ m/\Q$response\E/i) { # error message to client $c->push_write("\\echo\\333networks did not understand your request. ". @@ -91,7 +87,7 @@ sub handle_validate { my $val = 0; # pass or fail the secure challenge - if (exists $r->{gamename} && $self->get_game_props($r->{gamename})) { + if (exists $r->{gamename} && $self->get_game_props(gamename => $r->{gamename})) { # game exists and we have the key to verify the response $val = $self->compare_challenge( @@ -117,73 +113,9 @@ sub handle_validate { 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. -## TODO: deprecate about query -- info will now be in udp query! -################################################################################ -sub handle_about { - my ($self, $about, $a, $p) = @_; - my $response = ""; - - # contact info - if ($about =~ /^contact$/i or $about =~ /^undef$/i) { - $response .= "\\about\\$self->{masterserver_hostname}, contact: $self->{masterserver_contact}"; - $self->log("about","communicating to $a:$p my contact information."); - } - - # build/version info - if ($about =~ /^build$/i or $about =~ /^version$/i or $about =~ /^undef$/i) { - $response .= "\\build\\$self->{build_type} $self->{build_version} written " - . "by $self->{build_author}, released $self->{build_date}"; - $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}"; - $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]; - } - $response .= "\\support\\$sgs"; - $self->log("about","telling $a:$p which games are supported."); - } - - # unsupported query - if ("contact build address support version undef" !~ m/$about/i) { - $response .= "\\echo\\incorrect query usage, supported queries are: contact build version address support."; - $self->log("about","incorrect query \"$about\", telling $a:$p the supported \"about\" queries."); - } - - # 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) = @_; diff --git a/lib/MasterServer/TCP/ListCompiler.pm b/lib/MasterServer/TCP/ListCompiler.pm index 97ef541..c035008 100755 --- a/lib/MasterServer/TCP/ListCompiler.pm +++ b/lib/MasterServer/TCP/ListCompiler.pm @@ -98,7 +98,7 @@ sub compile_sync { for my $g (keys %games) { # $g is now a gamename -- check if it's supported. Else ignore. - if ($self->get_game_props($g)) { + if ($self->get_game_props(gamename => $g)) { # get list from database my $list = $self->get_server( diff --git a/lib/MasterServer/UDP/BeaconCatcher.pm b/lib/MasterServer/UDP/BeaconCatcher.pm index 5c4ff5f..7c98f57 100755 --- a/lib/MasterServer/UDP/BeaconCatcher.pm +++ b/lib/MasterServer/UDP/BeaconCatcher.pm @@ -65,8 +65,13 @@ sub on_beacon_receive { if ($b =~ m/\\heartbeat\\/ && $b =~ m/\\gamename\\/); # if other masterservers check if we're still alive - $self->process_udp_secure($udp, $pa, $b, $peer_addr) - if ($b =~ m/\\secure\\/ || $b =~ m/\\basic\\/ || $b =~ m/\\status\\/ || $b =~ m/\\info\\/); + $self->handle_status_query($udp, $pa, $b, $peer_addr) + if ($b =~ m/\\secure\\/ || + $b =~ m/\\basic\\/ || + $b =~ m/\\info\\/ || + $b =~ m/\\rules\\/ || + $b =~ m/\\players\\/|| + $b =~ m/\\status\\/); } 1; diff --git a/lib/MasterServer/UDP/DatagramProcessor.pm b/lib/MasterServer/UDP/DatagramProcessor.pm index 2a26763..006871d 100755 --- a/lib/MasterServer/UDP/DatagramProcessor.pm +++ b/lib/MasterServer/UDP/DatagramProcessor.pm @@ -34,13 +34,13 @@ sub process_udp_beacon { $self->log("beacon", "$peer_addr:$r{heartbeat} for $r{gamename}"); # check if game is actually supported in our db - my $game_props = $self->get_game_props($r{gamename}); + my $game_props = $self->get_game_props(gamename => $r{gamename})->[0]; # if no entry exists, report error. if (defined $game_props) { # validate heartbeat data - my $heartbeat = ($r{heartbeat} || $game_props->{default_qport}); + my $heartbeat = ($r{heartbeat} || ($game_props->{default_qport} || 0)); # # verify valid server address (ip+port) @@ -140,7 +140,7 @@ sub process_udp_validate { # verify challenge my $val = $self->compare_challenge( - gamename => $pending->{gamename}, + gamename => lc $pending->{gamename}, secure => $pending->{secure}, enctype => $r{enctype}, validate => $r{validate}, @@ -221,7 +221,7 @@ sub process_query_response { # check whether the gamename is supported in our db - if (exists $s{gamename} && $self->get_game_props($s{gamename})) { + if (exists $s{gamename} && $self->get_game_props(gamename => $s{gamename})) { # parse variables my %nfo = (); diff --git a/lib/MasterServer/UDP/UpLink.pm b/lib/MasterServer/UDP/UpLink.pm index 63cbefb..01e806e 100755 --- a/lib/MasterServer/UDP/UpLink.pm +++ b/lib/MasterServer/UDP/UpLink.pm @@ -9,8 +9,11 @@ use Exporter 'import'; our @EXPORT = qw| send_heartbeats do_uplink - process_uplink_response - process_udp_secure |; + process_udp_secure + handle_status_query |; + +# for compliance, query ID +my $query_id = 0; ################################################################################ ## Broadcast heartbeats to other masterservers @@ -57,7 +60,11 @@ sub do_uplink { timeout => $self->{timeout_time}, on_timeout => sub {$udp_client->destroy()}, on_error => sub {$udp_client->destroy()}, - on_recv => sub {$self->process_uplink_response(@_)}, + on_recv => sub { + my ($self, $buf, $udp, $pa) = @_; + $self->handle_status_query($udp, $pa, $buf) + if ($buf =~ m/secure/); + }, ); # Send heardbeat @@ -65,78 +72,101 @@ sub do_uplink { } ################################################################################ -## Process requests received after uplinking -## +## Respond to status-like queries. Supported queries are basic, info, rules, +## players, status. +## Note: this replaces the \about\ query in the TCP handler! ################################################################################ -sub process_uplink_response { - # $self, beacon address, handle, packed client address - my ($self, $b, $udp, $pa) = @_; - - # unpack ip from packed client address - my ($port, $iaddr) = sockaddr_in($pa); - my $peer_addr = inet_ntoa($iaddr); - - # assume fraud/crash attempt if response too long - if (length $b > 64) { - # log - $self->log("attack","length exceeded in uplink response: $peer_addr:$port sent $b"); - - # truncate and try to continue - $b = substr $b, 0, 64; - } - - # check if this is a secure challenge - $self->process_udp_secure($udp, $pa, $b, $peer_addr) - if ($b =~ m/\\secure\\/); -} +sub handle_status_query { + my ($self, $udp, $pa, $buf) = @_; - -################################################################################ -## Process the received secure query and respond with the correct response -## TODO: expand queries with support for info, rules, players, status, etc -################################################################################ -sub process_udp_secure { - # $self, handle, packed address, udp data, peer ip address, $port - my ($self, $udp, $pa, $buf, $peer_addr) = @_; - - # received secure in $buf: \basic\\secure\wookie + # hotfix for one-word queries + $buf .= "\\dummy\\"; my %r; - + $buf = encode('UTF-8', $buf); - $buf =~ s/\\\\/\\undef\\/; $buf =~ s/\n//; + $buf =~ s/\\\\/\\undef\\/g; # where to add the +? seperate perl script! $buf =~ s/\\([^\\]+)\\([^\\]+)/$r{$1}=$2/eg; # response string my $response = ""; - # compile basic string + # for compliance, query ids between 0-99 + $query_id = ($query_id >= 99) ? 1 : ++$query_id; + my $sub_id = 1; - # provide basic information if asked for - if (defined $r{basic} || defined $r{status} || defined $r{info}) { + # get database info to present game stats as players, where num_total > 0 + my $maxgames = $self->check_cipher_count(); + my $gameinfo = $self->get_game_props( + num_gt => 1, + sort => "num_total", + reverse => 1 + ); - # format: \gamename\ut\gamever\348\minnetver\348\location\0\final\\queryid\16.1 + # secure challenge + if (defined $r{secure}) { + $response .= "\\validate\\" + . $self->validate_string( + gamename => "333networks", + enctype => 0, + secure => $r{secure} + ); + } + + # basic query + if (defined $r{basic} || defined $r{status}) { $response .= "\\gamename\\333networks" . "\\gamever\\$self->{short_version}" . "\\location\\0" - . "\\hostname\\$self->{masterserver_hostname}" - . "\\hostport\\$self->{listen_port}"; + . "\\queryid\\$query_id.".$sub_id++; + } + + # info query + if (defined $r{info} || defined $r{status}) { + $response .= "\\hostname\\$self->{masterserver_hostname}" + . "\\hostport\\$self->{listen_port}" + . "\\gametype\\MasterServer" + . "\\numplayers\\". scalar @{$gameinfo} + . "\\maxplayers\\$maxgames" + . "\\gamemode\\openplaying" + . "\\queryid\\$query_id.".$sub_id++; + } + + # rules query + if (defined $r{rules} || defined $r{status}) { + $response .= "\\mutators\\333networks synchronization, master applet synchronization" + . "\\AdminName\\$self->{masterserver_name}" + . "\\AdminEMail\\$self->{masterserver_contact}" + . "\\queryid\\$query_id.".$sub_id++; } - # TODO: add queryid -- not because it's useful, but because protocol compliant + # players query + if (defined $r{players} || defined $r{status}) { + # list game stats as if they were players, with game description as + # player_$, gamename as skin_$, total servers as frags_$ and number of + # direct uplinks as deaths_$ + my $c = 0; - # support for secure/validate - if (defined $r{secure}) { - # generate response - - $response .= "\\validate\\" - . $self->validate_string(gamename => "333networks", - enctype => 0, - secure => $r{secure}); + foreach my $p (@{$gameinfo}) { + $c++; # count players + $response .= "\\player_$c\\$p->{description}" + . "\\skin_$c\\$p->{gamename}" + . "\\frags_$c\\$p->{num_total}" + . "\\deaths_$c\\$p->{num_uplink}"; + } + $response .= "\\queryid\\$query_id.".$sub_id++; } - # send the response - $udp->push_send("$response\\final\\", $pa); + # close query with final tag + $response .= "\\final\\"; + + # split the response in chunks of 512 bytes and send + while (length $response > 512) { + my $chunk = substr $response, 0, 512, ''; + $udp->push_send($chunk, $pa); + } + # last <512 chunk + $udp->push_send($response, $pa); } 1; |
