diff options
| author | Darkelarious <darkelarious@333networks.com> | 2017-05-13 14:18:28 +0200 |
|---|---|---|
| committer | Darkelarious <darkelarious@333networks.com> | 2017-05-13 14:20:49 +0200 |
| commit | 34a2c7390ea9662d33258d384e72fff1912343ff (patch) | |
| tree | d96ea33c0107e4906a152aa1de4b5c75b81ba0a8 /lib/MasterServer/Core | |
| parent | 84af66aba26d2088d5d95c240d176f3edaf17b58 (diff) | |
| download | MasterServer-Perl-34a2c7390ea9662d33258d384e72fff1912343ff.tar.gz MasterServer-Perl-34a2c7390ea9662d33258d384e72fff1912343ff.zip | |
revised synchronization methods, config settings and bug fixesv2.3.0
Diffstat (limited to 'lib/MasterServer/Core')
| -rwxr-xr-x | lib/MasterServer/Core/Core.pm | 32 | ||||
| -rwxr-xr-x | lib/MasterServer/Core/LoadConfig.pm | 150 | ||||
| -rwxr-xr-x | lib/MasterServer/Core/Logging.pm | 30 | ||||
| -rwxr-xr-x | lib/MasterServer/Core/Schedulers.pm | 117 | ||||
| -rwxr-xr-x | lib/MasterServer/Core/Secure.pm | 73 | ||||
| -rwxr-xr-x | lib/MasterServer/Core/Stats.pm | 4 | ||||
| -rwxr-xr-x | lib/MasterServer/Core/Util.pm | 11 | ||||
| -rwxr-xr-x | lib/MasterServer/Core/Version.pm | 7 |
8 files changed, 290 insertions, 134 deletions
diff --git a/lib/MasterServer/Core/Core.pm b/lib/MasterServer/Core/Core.pm index be0646b..4d1e47b 100755 --- a/lib/MasterServer/Core/Core.pm +++ b/lib/MasterServer/Core/Core.pm @@ -1,4 +1,3 @@ - package MasterServer::Core::Core; use strict; @@ -6,6 +5,7 @@ use warnings; use AnyEvent; use Exporter 'import'; use DBI; +$|++; our @EXPORT = qw | halt select_database_type main |; @@ -47,7 +47,7 @@ sub select_database_type { if ( "Pg SQLite mysql" =~ m/$db_type[1]/i) { # inform us what DB we try to load - $self->log("load","Loading $db_type[1] database module."); + $self->log("debug","Loading $db_type[1] database module."); # load dbd and tables/queries for this db type MasterServer::load_recursive("MasterServer::Database::$db_type[1]"); @@ -74,11 +74,11 @@ sub main { # condition var prevents or allows the program from ending $self->{must_halt} = AnyEvent->condvar; - # force version info + # load version info $self->version(); # print startup - print "Running...\n"; + print "Running 333networks Master Server Application...\n"; # keep several objects alive outside their original scope $self->{scope} = (); @@ -96,14 +96,24 @@ sub main { # determine the type of database and load the appropriate module $self->select_database_type(); + ### # - # Prepare necessary tasks for running the masterserver + # execute necessary tasks for running the masterserver # + ### + + # load the list with ciphers from the config file if no ciphers were detected + # update manually with util/tools/db_load_ciphers.pl + # then unload the game variables from masterserver memory + $self->load_ciphers() unless $self->check_cipher_count(); + $self->{game} = undef; - # (re)load the list with ciphers from the config file, into the database - $self->load_ciphers(); + # (re)load the list with masterservers and master applets from config + # does not clear out old entries, but resets "last_updated" to now + $self->load_sync_masters(); + $self->load_applet_masters(); - # set first run flag to avoid ignoring servers after downtime + # set first run flag to avoid ignoring/deleting servers after downtime $self->{firstrun} = undef; $self->{firstruntime} = time; @@ -134,10 +144,14 @@ sub main { # provide server lists to clients with the browser host server $self->{scope}->{browser_host} = $self->browser_host(); + ### + # # all modules loaded. Running... + # + ### $self->log("info", "All modules loaded. Masterserver is now running."); - # prevent main program from ending prematurely + # prevent main program from ending as long as no fatal errors occur $self->{must_halt}->recv; } diff --git a/lib/MasterServer/Core/LoadConfig.pm b/lib/MasterServer/Core/LoadConfig.pm new file mode 100755 index 0000000..e209848 --- /dev/null +++ b/lib/MasterServer/Core/LoadConfig.pm @@ -0,0 +1,150 @@ +package MasterServer::Core::LoadConfig; + +use strict; +use warnings; +use AnyEvent; +use POSIX qw/strftime/; +use Exporter 'import'; +use DBI; + +our @EXPORT = qw | load_applet_masters + load_sync_masters + add_sync_master |; + +################################################################################ +## Load configuration variables to the database, helper functions +################################################################################ +sub load_applet_masters { + my $self = shift; + + # loop through config entries + foreach my $master_applet (@{$self->{master_applet}}) { + # master_applet contains + # address --> domain + # port --> tcp port + # games --> array of gamenames + + # iterate through all games per entry + for my $gamename (@{$master_applet->{games}}) { + + # resolve domain names + my $applet_ip = $self->host2ip($master_applet->{address}); + + # check if all credentials are valid + if ($applet_ip && + $master_applet->{port} && + $gamename) + { + # add to database + $self->add_master_applet( + ip => $applet_ip, + port => $master_applet->{port}, + gamename => $gamename, + ); + + #log + $self->log("add", "added applet $master_applet->{address}:$master_applet->{port} for $gamename"); + + } # else: insufficient info available + else { + $self->log("fail", "Could not add master applet: ". + ($applet_ip || "unknown ip"). ", ". + ($master_applet->{port} || "0"). ", ". + ($gamename || "game"). "." + ); + } + } # end gamename + } # end master_applet + + # reset added/updated time to last current time + $self->reset_master_applets(); + + # clear out the original variable, we don't use it anymore + $self->{master_applet} = (); + + # report + $self->log("info", "Applet database successfully updated!"); + +} + +################################################################################ +## There are three ways to load new masterservers to sync with. +## 1: from the config file; address, port and beaconport are provided +## 2: from a heartbeat; this automatically parses like all other servers +## 3: from another sync request. Add if sufficient info is available +################################################################################ +sub load_sync_masters { + my $self = shift; + + # loop through config entries + foreach my $sync_host (@{$self->{sync_masters}}) { + + # add them to database + $self->add_sync_master($sync_host); + } + + # clear out the original variable, we don't use it anymore + $self->{sync_masters} = (); + + # report + $self->log("info", "Sync server database successfully updated!"); + +} + +################################################################################ +## Add a sync master according to cases 1 and 3. +## Check for valid IP, port and/or beaconport +################################################################################ +sub add_sync_master { + my ($self, $sync_host) = @_; + + # sync_host contains + # address --> domain + # port --> tcp port + # beacon --> udp port + + # resolve domain names + my $sync_ip = $self->host2ip($sync_host->{address}); + + # check if all credentials are valid + if ($sync_ip && + $sync_host->{beacon} && + $sync_host->{port}) + { + # select sync master from serverlist + my $entry = $self->get_server(ip => $sync_ip, + port => $sync_host->{beacon})->[0]; + + # was found, update the entry + if (defined $entry) { + # update the serverlist with + my $sa = $self->update_server_list( + ip => $sync_ip, + port => $sync_host->{beacon}, + hostport => $sync_host->{port}, + gamename => "333networks", + ); + } + # was not found, insert clean entry + else { + my $sa = $self->add_server_list( + ip => $sync_ip, + port => $sync_host->{beacon}, + hostport => $sync_host->{port}, + gamename => "333networks", + ); + + #log + $self->log("add", "added sync $sync_host->{address}:$sync_host->{port},$sync_host->{beacon}"); + } + } # else: insufficient info available + else { + $self->log("fail", "Could not add sync master: ". + ($sync_ip || "ip"). ", ". + ($sync_host->{beacon} || "0"). ", ". + ($sync_host->{port} || "0"). "." + ); + } +} + +1; diff --git a/lib/MasterServer/Core/Logging.pm b/lib/MasterServer/Core/Logging.pm index e8631de..416a97f 100755 --- a/lib/MasterServer/Core/Logging.pm +++ b/lib/MasterServer/Core/Logging.pm @@ -1,4 +1,3 @@ - package MasterServer::Core::Logging; use strict; @@ -6,6 +5,7 @@ use warnings; use Switch; use POSIX qw/strftime/; use Exporter 'import'; +$|++; our @EXPORT = qw| log error |; @@ -46,41 +46,34 @@ sub error { } } - ################################################################################ ## Log to file and print to screen. ## args: $self, message_type, message ################################################################################ sub log { my ($self, $type, $msg) = @_; - - # flush - $| = 1; - - # parse time of log entry and prep for rotating log - my $time = strftime('%Y-%m-%d %H:%M:%S',localtime); - my $yearly = strftime('-%Y',localtime); - my $monthly = strftime('-%Y-%m',localtime); - my $weekly = strftime('-%Y-week%U',localtime); - my $daily = strftime('-%Y-%m-%d',localtime); - + # is the message suppressed in config? return if (defined $type && $self->{suppress} =~ m/$type/i); + + # parse time of log entry and prep for rotating log + my $time = strftime('%Y-%m-%d %H:%M:%S',localtime); # determine filename my $f = "MasterServer"; # rotate log filename according to config - $f .= $daily if ($self->{log_rotate} =~ /^daily$/i ); - $f .= $weekly if ($self->{log_rotate} =~ /^weekly$/i ); - $f .= $monthly if ($self->{log_rotate} =~ /^monthly$/i ); - $f .= $yearly if ($self->{log_rotate} =~ /^yearly$/i ); + $f .= strftime('-%Y-%m-%d',localtime) if ($self->{log_rotate} =~ /^daily$/i ); + $f .= strftime('-%Y-week%U',localtime) if ($self->{log_rotate} =~ /^weekly$/i ); + $f .= strftime('-%Y-%m',localtime) if ($self->{log_rotate} =~ /^monthly$/i); + $f .= strftime('-%Y',localtime) if ($self->{log_rotate} =~ /^yearly$/i ); $f .= ".log"; # put log filename together my $logfile = $self->{log_dir}.((substr($self->{log_dir},-1) eq "/")?"":"/").$f; - print "[$time] [$type] > $msg\n" if $self->{printlog}; + # print to stdout if enabled + print "[$time]\t[$type]\t$msg\n" if $self->{printlog}; # temporarily disable the warnings-to-log, to avoid infinite recursion if # this function throws a warning. @@ -99,5 +92,4 @@ sub log { $SIG{__WARN__} = $old; } - 1; diff --git a/lib/MasterServer/Core/Schedulers.pm b/lib/MasterServer/Core/Schedulers.pm index 97e45a5..cee4e5c 100755 --- a/lib/MasterServer/Core/Schedulers.pm +++ b/lib/MasterServer/Core/Schedulers.pm @@ -1,4 +1,3 @@ - package MasterServer::Core::Schedulers; use strict; @@ -18,75 +17,90 @@ our @EXPORT = qw | ################################################################################ sub long_periodic_tasks { my $self = shift; - my $num = 0; + my $prev = 0; return AnyEvent->timer ( - after => 300, # 5 minutes grace time - interval => 1800, # execute every half hour + after => 30, # 30 seconds grace time + interval => 3600, # execute every hour cb => sub { - ## update Killing Floor stats - $self->read_kfstats(); + # update Killing Floor stats + $self->read_kfstats() if $self->{kfstats_enabled}; + + # delete old masterserver applets that have been unresponsive for a while now + $self->remove_unresponsive_applets() if (defined $self->{firstrun}); # time spacer my $t = 0; # clean out handles from the previous round (executed or not) $self->{scope}->{sync} = (); - - ## Query Epic Games'-based UCC applets periodically to get an additional - ## list of online UT, Unreal (or other) game servers. - if ($self->{master_applet_enabled}) { - for my $ms (@{$self->{master_applet}}) { + + # Synchronize with all other 333networks masterservers that are uplinking, + # added by synchronization or manually listed. + if ($self->{sync_enabled}) { + + # get serverlist + my $masterserverlist = $self->get_server( + updated => 3600, + gamename => "333networks", + ); - # add 3 second delay to spread network/server load + foreach my $ms (@{$masterserverlist}) { + # add 5 second delay to spread network/server load $self->{scope}->{sync}->{$t} = AnyEvent->timer( - after => 3*$t++, - cb => sub{$self->query_applet($ms)} - ); + after => 5*$t++, + cb => sub{$self->sync_with_master($ms)} + ) if ($ms->{hostport} > 0); } } + + # do NOT reset $t, keep padding time -- you should not have more than 300 + # entries in applets/syncer in total anyway. - # do NOT reset $t, keep padding time -- you should not have more than 600 - # entries in applets/syncer in total. - - ## Request the masterlist for selected or all games from other - ## 333networks-based masterservers that uplinked to us and otherwise made - ## our list (config, manual entry, etc) - if ($self->{sync_enabled}) { - foreach my $ms (values %{$self->masterserver_list()}) { + # Query Epic Games-based UCC applets periodically to get an additional + # list of online UT, Unreal and other game servers. + if ($self->{master_applet_enabled}) { - # add 3 second delay to spread network/server load + # get applet list + my $appletlist = $self->get_masterserver_applets(); + + for my $ms (@{$appletlist}) { + + # add 5 second delay to spread network/server load $self->{scope}->{sync}->{$t} = AnyEvent->timer( - after => 3*$t++, - cb => sub{$self->sync_with_master($ms) if ($ms->{tcp} > 0)} + after => 5*$t++, + cb => sub{$self->query_applet($ms)} ); } } # - # Also very long-running tasks, like once per day: + # very long-running tasks, like database dumps + # interval from config # - if ($num++ >= 47) { - # reset counter - $num = 0; - - # - # do database dump - # - my $time = strftime('%Y-%m-%d-%H-%M',localtime); - - # read db type from db login - my @db_type = split(':', $self->{dblogin}->[0]); - $db_type[2] =~ s/dbname=//; + my $curr = 0; + $curr = strftime('%d',localtime) if ($self->{dump_db} =~ /^daily$/i ); + $curr = strftime('%U',localtime) if ($self->{dump_db} =~ /^weekly$/i ); + $curr = strftime('%m',localtime) if ($self->{dump_db} =~ /^monthly$/i); + $curr = strftime('%Y',localtime) if ($self->{dump_db} =~ /^yearly$/i ); + + # on change, execute + if ($prev < $curr) { - if ($db_type[1] eq "Pg") { - # use pg_dump to dump Postgresql databases - system("pg_dump $db_type[2] -U $self->{dblogin}->[1] > $self->{root}/data/dumps/$db_type[1]-$time.db"); - $self->log("dump", "Dumping database to /data/dumps/$db_type[1]-$time.db"); + # skip on first run + if ($prev == 0) { + # update timer and loop + $prev = $curr; + return; } + + # dump db + $self->dump_database(); + + # update timekeeper + $prev = $curr; } - }, ); } @@ -102,22 +116,19 @@ sub short_periodic_tasks { interval => 120, cb => sub { - ## update stats on direct beacons and total number of servers + # update stats on direct beacons and total number of servers $self->update_stats(); - ## determine whether servers are still uplinking to us. If not, toggle. + # determine whether servers are still uplinking to us. If not, toggle. $self->write_direct_beacons() if (defined $self->{firstrun}); - ## delete old servers from the "pending" list (except for the first run) + # delete old servers from the "pending" list (except for the first run) $self->delete_old_pending() if (defined $self->{firstrun}); - ## uplink to other 333networks masterservers with heartbeats, - ## that way we can index other masterservers too + # uplink to other 333networks masterservers with heartbeats, so other + # masterservers can find us too $self->send_heartbeats(); - - # - # more short tasks? - # + }, ); } diff --git a/lib/MasterServer/Core/Secure.pm b/lib/MasterServer/Core/Secure.pm index 51d1832..6d05f82 100755 --- a/lib/MasterServer/Core/Secure.pm +++ b/lib/MasterServer/Core/Secure.pm @@ -1,4 +1,3 @@ - package MasterServer::Core::Secure; use strict; @@ -25,7 +24,7 @@ sub load_ciphers { # first delete the old cipher database $self->clear_ciphers(); - # start inserting ciphers (lots of 'em) + # start inserting ciphers (use transactions for slow systems) $self->{dbh}->begin_work; # iterate through the game list @@ -36,10 +35,11 @@ sub load_ciphers { $opt{gamename} = lc $_; $opt{cipher} = $self->{game}->{$_}->{key}; $opt{description} = $self->{game}->{$_}->{label} || 'Unknown Game'; - $opt{default_qport} = $self->{game}->{$_}->{port} || 0; + $opt{default_qport} = $self->{game}->{$_}->{port} || 0; # 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(); } @@ -48,19 +48,14 @@ sub load_ciphers { # commit $self->{dbh}->commit; $self->log("info", "Cipher database successfully updated!"); - - # unload the game variables from masterserver memory - $self->{game} = undef; - } - ################################################################################ # generate a random string of 6 characters long for the \secure\ challenge # returns string ################################################################################ sub secure_string { - # spit out a random string, only uppercase characters + # generate a random string, only uppercase characters my @c = ('A'..'Z'); my $s = ""; $s .= $c[rand @c] for 1..6; @@ -82,23 +77,21 @@ sub compare_challenge { # secure string too long? (because vulnerable in UE) return 0 if (length $o{secure} > 16); - # additional conditions to skip checking provided? - $o{ignore} = "" unless $o{ignore}; - # ignore this game if asked to do so - if ($o{ignore} =~ m/$o{gamename}/i){ - $self->log("secure", "ignored beacon validation for $o{gamename}"); + 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}; - - # get cipher corresponding with the gamename - my $cip = $self->get_game_props($o{gamename})->{cipher}; - + # calculate validate string - my $val = get_validate_string($cip, $o{secure}, $o{enctype}); + my $val = get_validate_string( + $self->get_game_props($o{gamename})->{cipher}, + $o{secure}, + $o{enctype} + ); # return whether or not they match return ($val eq $o{validate}); @@ -136,33 +129,33 @@ sub validate_string { # conversion and modification of the algorithm by Darkelarious, June 2014 with # explicit, written permission of Luigi Auriemma. # +# use pre-built rotations for enctype +# -- see GSMSALG 0.3.3 reference for copyright and more information +my @enc_chars = ( qw | + 001 186 250 178 081 000 084 128 117 022 142 142 002 008 054 165 + 045 005 013 022 082 007 180 034 140 233 009 214 185 038 000 004 + 006 005 000 019 024 196 030 091 029 118 116 252 080 081 006 022 + 000 081 040 000 004 010 041 120 081 000 001 017 082 022 006 074 + 032 132 001 162 030 022 071 022 050 081 154 196 003 042 115 225 + 045 079 024 075 147 076 015 057 010 000 004 192 018 012 154 094 + 002 179 024 184 007 012 205 033 005 192 169 065 067 004 060 082 + 117 236 152 128 029 008 002 029 088 132 001 078 059 106 083 122 + 085 086 087 030 127 236 184 173 000 112 031 130 216 252 151 139 + 240 131 254 014 118 003 190 057 041 119 048 224 043 255 183 158 + 001 004 248 001 014 232 083 255 148 012 178 069 158 010 199 006 + 024 001 100 176 003 152 001 235 002 176 001 180 018 073 007 031 + 095 094 093 160 079 091 160 090 089 088 207 082 084 208 184 052 + 002 252 014 066 041 184 218 000 186 177 240 018 253 035 174 182 + 069 169 187 006 184 136 020 036 169 000 020 203 036 018 174 204 + 087 086 238 253 008 048 217 253 139 062 010 132 070 250 119 184 +|); +# # args: game cipher, 6-char challenge string, encryption type # returns: validate string (usually 8 characters long) # !! requires cipher hash to be configured in config! (imported or otherwise) ################################################################################ sub get_validate_string { my ($cipher_string, $secure_string, $enctype) = @_; - - # use pre-built rotations for enctype - # -- see GSMSALG 0.3.3 reference for copyright and more information - my @enc_chars = ( qw | - 001 186 250 178 081 000 084 128 117 022 142 142 002 008 054 165 - 045 005 013 022 082 007 180 034 140 233 009 214 185 038 000 004 - 006 005 000 019 024 196 030 091 029 118 116 252 080 081 006 022 - 000 081 040 000 004 010 041 120 081 000 001 017 082 022 006 074 - 032 132 001 162 030 022 071 022 050 081 154 196 003 042 115 225 - 045 079 024 075 147 076 015 057 010 000 004 192 018 012 154 094 - 002 179 024 184 007 012 205 033 005 192 169 065 067 004 060 082 - 117 236 152 128 029 008 002 029 088 132 001 078 059 106 083 122 - 085 086 087 030 127 236 184 173 000 112 031 130 216 252 151 139 - 240 131 254 014 118 003 190 057 041 119 048 224 043 255 183 158 - 001 004 248 001 014 232 083 255 148 012 178 069 158 010 199 006 - 024 001 100 176 003 152 001 235 002 176 001 180 018 073 007 031 - 095 094 093 160 079 091 160 090 089 088 207 082 084 208 184 052 - 002 252 014 066 041 184 218 000 186 177 240 018 253 035 174 182 - 069 169 187 006 184 136 020 036 169 000 020 203 036 018 174 204 - 087 086 238 253 008 048 217 253 139 062 010 132 070 250 119 184 - |), # convert to array of characters my @cip = split "", $cipher_string; diff --git a/lib/MasterServer/Core/Stats.pm b/lib/MasterServer/Core/Stats.pm index 25044e8..8e9eb95 100755 --- a/lib/MasterServer/Core/Stats.pm +++ b/lib/MasterServer/Core/Stats.pm @@ -1,4 +1,3 @@ - package MasterServer::Core::Stats; use strict; @@ -31,9 +30,8 @@ sub update_stats { $self->write_stat(%opt); } - #done + # done $self->log("stat", "Updated all game statistics."); - } 1; diff --git a/lib/MasterServer/Core/Util.pm b/lib/MasterServer/Core/Util.pm index 4f64fe1..682335c 100755 --- a/lib/MasterServer/Core/Util.pm +++ b/lib/MasterServer/Core/Util.pm @@ -1,4 +1,3 @@ - package MasterServer::Core::Util; use strict; @@ -24,10 +23,10 @@ sub ip2country { ################################################################################ sub host2ip { my ($self, $name) = @_; - return inet_ntoa(inet_aton($name)) if $name; + my $unpack = inet_aton($name) if $name; + return inet_ntoa($unpack) if $unpack; } - ################################################################################ ## Verify whether a given domain name or IP address and port are valid. ## returns 1/0 if valid/invalid ip + port. IPv4 ONLY! @@ -36,8 +35,8 @@ sub valid_address { my ($self, $a, $p) = @_; # check if ip and port are in valid range - my $val_addr = ($a =~ '\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b') if $a; - my $val_port = (0 < $p && $p <= 65535) if $p; + my $val_addr = ($a =~ '^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$') if $a; + my $val_port = ($p =~ m/^\d+$/ && 0 < $p && $p <= 65535) if $p; # exclude addresses where we don't want people sniffing for (qw|192.168.(.\d*).(.\d*) 127.0.(.\d*).(.\d*) 10.0.(.\d*).(.\d*)|){$val_addr = 0 if ($a =~ m/$_/)} @@ -72,7 +71,7 @@ sub db_all { } ################################################################################ -# sqlprint (TUWF): +# sqlprint (TUWF, Yorhel): # ? normal placeholder # !l list of placeholders, expects arrayref # !H list of SET-items, expects hashref or arrayref: format => (bind_value || \@bind_values) diff --git a/lib/MasterServer/Core/Version.pm b/lib/MasterServer/Core/Version.pm index 718b8c6..f87ea8d 100755 --- a/lib/MasterServer/Core/Version.pm +++ b/lib/MasterServer/Core/Version.pm @@ -1,4 +1,3 @@ - package MasterServer::Core::Version; use strict; @@ -27,16 +26,16 @@ sub version { # # master type - $self->{build_type} = "333networks Masterserver-Perl pre-release"; + $self->{build_type} = "333networks Masterserver-Perl Multidb"; # version - $self->{build_version} = "2.2.5"; + $self->{build_version} = "2.3.0"; # short version for uplinks $self->{short_version} = "MS-perl $self->{build_version}"; # date yyyy-mm-dd - $self->{build_date} = "2016-11-19"; + $self->{build_date} = "2017-05-13"; #author, email $self->{build_author} = "Darkelarious, darkelarious\@333networks.com"; |
