diff options
39 files changed, 4190 insertions, 0 deletions
@@ -0,0 +1,25 @@ +Copyright (c) 2005-2017 Darkelarious & 333networks.com + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +All Unreal Tournament related materials are based on Epic Games' Unreal +Tournament. UNREAL (c)1999 Epic Megagames, Inc. All Rights Reserved. Distributed +by GT Interactive Software, Inc. under license. UNREAL and the UNREAL logo are +registered trademarks of Epic Megagames, Inc. @@ -0,0 +1,113 @@ +========= + +DESCRIPTION + + This repository contains software for a web interface to display information + obtained by the 333networks MasterServer package for the support of various + legacy games. The software was written Darkelarious to soften the effects of + GameSpy (GameSpy Industries, Inc.) shutting down their masterserver service. + + A masterserver is a program that maintains a list of online game servers and + presents this list to clients (gamers, players) who request the list of game + addresses. The 333networks Masterserver is a software framework that allows + gamers/players to browse online games. + + More information about the masterserver and variations on the protocol by + 333networks can be found online at + http://333networks.com/masterserver + http://wiki.333networks.com/index.php/MasterServer + +AUTHOR + Darkelarious + http://333networks.com + darkelarious@333networks.com + +REQUIREMENTS + - Apache/httpd + - Postgresql, MySQL or SQLite3 + - Perl 5.10 or above + - The following CPAN modules: + DBI + DBD::Pg / DBD::SQLite / DBD::mysql + IP::Country::Fast + TUWF (http://dev.yorhel.nl/tuwf) + +INSTALL + + THE MASTER SERVER IS WRITTEN ON LINUX. IF YOU WANT TO RUN THE SOFTWARE IN + MICROSOFT WINDOWS OR APPLE OSX, IT MAY NOT WORK WITHOUT MODIFICATIONS. + + This repository consists of Perl modules and is run by a http deamon. First, + the MasterServer-Perl repository should be installed and configured in order + to run this web interface. The contents of this repository can be extracted + in the same root folder as MasterServer-Perl. + +CONFIGURATION + + The 333networks masterserver interface comes with options. These options are + found in configuration file "data/webinterface-config.pl". Comments in the + file give a brief description. Below, the configuration is discussed in + further detail. + + Database login information + + The masterserver interface supports different database types. This must be the + same database (and type) as you specified in your MasterServer-Perl config. + + # postgresql + db_login => ['dbi:Pg:dbname=masterserver', 'user', 'password'], + + # sqlite + db_login => ["dbi:SQLite:dbname=$ROOT/data/masterserver.db",'',''], + + You also need to (un)comment the right database module in the + masterwebinterface.pl file to load the correct driver. + + Apache settings + + LoadModule rewrite_module modules/mod_rewrite.so + AddHandler cgi-script .cgi .pl + + Vhost configuration for the Web Interface (assuming you extracted it in the + same folder as your MasterServer-Perl installation): + + # + # Master Web Interface + # + <VirtualHost *:80> + ServerAdmin master@yourdomain.com + ServerName master.yourdomain.com + + DocumentRoot "/server/MasterServer-Perl/s" + AddHandler cgi-script .pl + + RewriteEngine On + RewriteCond "%{DOCUMENT_ROOT}/%{REQUEST_URI}" !-s + RewriteRule ^/ /masterwebinterface.pl + + ErrorLog /server/MasterServer-Perl/log/MasterWebInterface-Error.log + CustomLog /server/MasterServer-Perl/log/MasterWebInterface-Access.log combined + + <Directory "/server/MasterServer-Perl/s"> + Options +FollowSymLinks +ExecCGI + AllowOverride None + Require all granted + </Directory> + </VirtualHost> + +KNOWN ISSUES + There are a few known issues that will be resolved in future versions. The + following issues are listed and do not need to be reported. + + This README file does not describe all possible configuration options. There + is an initiative to write the webinterface documentation in a single document + that is focused on all options and recommended values. For now, use your best + guess. + + There are no textures, map thumbnails and/or nice steals included in this + repository. All styles, textures, increasingly growing amounts of map thumbs + and game icons are located at another repository on git.333networks.com and + are available for all supported packages. + +COPYING + See COPYING file diff --git a/data/sql/tables-Pg.sql b/data/sql/tables-Pg.sql new file mode 100755 index 0000000..96f8f91 --- /dev/null +++ b/data/sql/tables-Pg.sql @@ -0,0 +1,103 @@ +CREATE TABLE appletlist( + id SERIAL UNIQUE NOT NULL PRIMARY KEY, + ip inet NOT NULL, + hostport INTEGER NOT NULL, + gamename TEXT NOT NULL DEFAULT '', + added timestamptz NOT NULL DEFAULT NOW(), + updated timestamptz NOT NULL DEFAULT NOW() +); + +CREATE TABLE serverlist( + id SERIAL UNIQUE NOT NULL PRIMARY KEY, + ip inet NOT NULL, + port INTEGER NOT NULL DEFAULT 0, + gamename TEXT NOT NULL DEFAULT '', + gamever TEXT, + hostname TEXT, + hostport INTEGER DEFAULT 0, + country TEXT, + b333ms BOOLEAN DEFAULT FALSE, + blacklisted BOOLEAN DEFAULT FALSE, + added timestamptz NOT NULL DEFAULT NOW(), + beacon timestamptz NOT NULL DEFAULT NOW(), + updated timestamptz NOT NULL DEFAULT NOW() +); +CREATE INDEX pendingaddress ON serverlist(ip, port); +CREATE INDEX updateaddress ON serverlist(ip, hostport); +CREATE INDEX directbeacons ON serverlist(gamename, b333ms); + +CREATE TABLE games( + gamename TEXT NOT NULL, + cipher TEXT, + description TEXT, + default_qport INTEGER DEFAULT 0, + num_uplink INTEGER DEFAULT 0, + num_total INTEGER DEFAULT 0 +); +CREATE INDEX gameprops ON games(gamename); + +CREATE TABLE pending( + id SERIAL UNIQUE NOT NULL PRIMARY KEY, + ip inet NOT NULL, + heartbeat INTEGER NOT NULL DEFAULT 0, + added timestamptz NOT NULL DEFAULT NOW() +); + +CREATE TABLE extended_info( + server_id SERIAL REFERENCES serverlist(id), + minnetver TEXT, + location TEXT, + listenserver TEXT, + adminname TEXT, + adminemail TEXT, + password TEXT, + gametype TEXT, + gamestyle TEXT, + changelevels TEXT, + maptitle TEXT, + mapname TEXT, + numplayers INTEGER DEFAULT 0, + maxplayers INTEGER DEFAULT 0, + minplayers INTEGER DEFAULT 0, + botskill TEXT, + balanceteams TEXT, + playersbalanceteams TEXT, + friendlyfire TEXT, + maxteams TEXT, + timelimit TEXT, + goalteamscore TEXT, + fraglimit TEXT, + mutators TEXT DEFAULT 'None', + updated timestamptz NOT NULL DEFAULT NOW() +); + +CREATE TABLE player_info( + server_id SERIAL NOT NULL, + player TEXT DEFAULT 'Player', + team TEXT, + frags INTEGER DEFAULT 0, + mesh TEXT, + skin TEXT, + face TEXT, + ping INTEGER DEFAULT 0, + ngsecret TEXT, + updated timestamptz NOT NULL DEFAULT NOW() +); + +CREATE TABLE kfstats( + UTkey TEXT NOT NULL, + Username TEXT DEFAULT ' ', + CurrentVeterancy TEXT DEFAULT 'None', + TotalKills INTEGER NOT NULL DEFAULT 0, + DecaptedKills INTEGER NOT NULL DEFAULT 0, + TotalMeleeDamage INTEGER NOT NULL DEFAULT 0, + MeleeKills INTEGER NOT NULL DEFAULT 0, + PowerWpnKills INTEGER NOT NULL DEFAULT 0, + BullpupDamage INTEGER NOT NULL DEFAULT 0, + StalkerKills INTEGER NOT NULL DEFAULT 0, + TotalWelded INTEGER NOT NULL DEFAULT 0, + TotalHealed INTEGER NOT NULL DEFAULT 0, + TotalPlaytime INTEGER NOT NULL DEFAULT 0, + GamesWon INTEGER NOT NULL DEFAULT 0, + GamesLost INTEGER NOT NULL DEFAULT 0 +); diff --git a/data/sql/tables-SQLite.sql b/data/sql/tables-SQLite.sql new file mode 100755 index 0000000..5608a66 --- /dev/null +++ b/data/sql/tables-SQLite.sql @@ -0,0 +1,104 @@ +CREATE TABLE appletlist( + id INTEGER PRIMARY KEY AUTOINCREMENT, + ip inet NOT NULL, + hostport INTEGER NOT NULL, + gamename TEXT NOT NULL DEFAULT '', + added timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE serverlist( + id INTEGER PRIMARY KEY AUTOINCREMENT, + ip inet NOT NULL, + port INTEGER NOT NULL DEFAULT 0, + gamename TEXT NOT NULL DEFAULT '', + gamever TEXT, + hostname TEXT, + hostport INTEGER DEFAULT 0, + country TEXT, + b333ms INTEGER DEFAULT 0, + blacklisted INTEGER DEFAULT 0, + added timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, + beacon timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX pendingaddress ON serverlist(ip, port); +CREATE INDEX updateaddress ON serverlist(ip, hostport); +CREATE INDEX directbeacons ON serverlist(gamename, b333ms); + +CREATE TABLE games( + gamename TEXT NOT NULL, + cipher TEXT, + description TEXT, + default_qport INTEGER DEFAULT 0, + num_uplink INTEGER DEFAULT 0, + num_total INTEGER DEFAULT 0 +); +CREATE INDEX gameprops ON games(gamename); + +CREATE TABLE pending( + id INTEGER PRIMARY KEY AUTOINCREMENT, + ip inet NOT NULL, + heartbeat INTEGER NOT NULL DEFAULT 0, + added timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE extended_info( + server_id INTEGER PRIMARY KEY AUTOINCREMENT, + minnetver TEXT, + location TEXT, + listenserver TEXT, + adminname TEXT, + adminemail TEXT, + password TEXT, + gametype TEXT, + gamestyle TEXT, + changelevels TEXT, + maptitle TEXT, + mapname TEXT, + numplayers INTEGER DEFAULT 0, + maxplayers INTEGER DEFAULT 0, + minplayers INTEGER DEFAULT 0, + botskill TEXT, + balanceteams TEXT, + playersbalanceteams TEXT, + friendlyfire TEXT, + maxteams TEXT, + timelimit TEXT, + goalteamscore TEXT, + fraglimit TEXT, + mutators TEXT DEFAULT 'None', + updated timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY(server_id) REFERENCES serverlist(id) +); + +CREATE TABLE player_info( + server_id INTEGER NOT NULL, + player TEXT DEFAULT 'Player', + team TEXT, + frags INTEGER DEFAULT 0, + mesh TEXT, + skin TEXT, + face TEXT, + ping INTEGER DEFAULT 0, + ngsecret TEXT, + updated timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE kfstats( + UTkey TEXT NOT NULL, + Username TEXT DEFAULT ' ', + CurrentVeterancy TEXT DEFAULT 'None', + TotalKills INTEGER NOT NULL DEFAULT 0, + DecaptedKills INTEGER NOT NULL DEFAULT 0, + TotalMeleeDamage INTEGER NOT NULL DEFAULT 0, + MeleeKills INTEGER NOT NULL DEFAULT 0, + PowerWpnKills INTEGER NOT NULL DEFAULT 0, + BullpupDamage INTEGER NOT NULL DEFAULT 0, + StalkerKills INTEGER NOT NULL DEFAULT 0, + TotalWelded INTEGER NOT NULL DEFAULT 0, + TotalHealed INTEGER NOT NULL DEFAULT 0, + TotalPlaytime INTEGER NOT NULL DEFAULT 0, + GamesWon INTEGER NOT NULL DEFAULT 0, + GamesLost INTEGER NOT NULL DEFAULT 0 +); diff --git a/data/webinterface-config.pl b/data/webinterface-config.pl new file mode 100755 index 0000000..fea3605 --- /dev/null +++ b/data/webinterface-config.pl @@ -0,0 +1,19 @@ +package MasterWebInterface; +our(%S, $ROOT); +our %S = ( + root => $ROOT, + + url => 'http://simple.333networks.com', + admin_email => 'info@333networks.com', + + site_title => '333networks Masterserver WebInterface', + site_name => '333networks', + + style => 'classic', + img_path => "$ROOT/s/img", + + map_url => "/map", + map_dir => "$ROOT/s/map", +); + +1; diff --git a/lib/MasterWebInterface/Database/Pg/Games.pm b/lib/MasterWebInterface/Database/Pg/Games.pm new file mode 100755 index 0000000..38650b2 --- /dev/null +++ b/lib/MasterWebInterface/Database/Pg/Games.pm @@ -0,0 +1,55 @@ +package MasterWebInterface::Database::Pg::Games; +use strict; +use warnings; +use Exporter 'import'; +our @EXPORT = qw| dbGameListGet dbGetGameDesc |; + +################################################################################ +## get list of game details +## opt: filter first letter +################################################################################ +sub dbGameListGet { + my $s = shift; + my %o = (page => 1, results => 50, sort => '', @_); + + my %where = ( + $o{firstchar} + ? ('upper(SUBSTRING(description from 1 for 1)) = ?' => $o{firstchar} ) : (), + !$o{firstchar} && defined $o{firstchar} + ? ('ASCII(description) < 97 OR ASCII(description) > 122' => 1 ) : (), + $o{search} + ? ('description ILIKE ?' => "%$o{search}%") : (), + ); + + my @select = ( qw| description gamename num_uplink num_total |); + my $order = sprintf { + description => 'description %s', + gamename => 'gamename %s', + num_uplink => 'num_uplink %s', + num_total => 'num_total %s', + }->{ $o{sort}||'num_total' }, $o{reverse} ? 'DESC' : 'ASC'; + + my($r, $np) = $s->dbPage(\%o, q| + SELECT !s FROM games + !W + ORDER BY !s|, + join(', ', @select), \%where, $order + ); + + my $p = $s->dbAll( q| + SELECT COUNT(*) AS num + FROM games + !W|, \%where, + )->[0]{num}; + return wantarray ? ($r, $np, $p) : $r; +} + +################################################################################ +## get description for a game by gamename +################################################################################ +sub dbGetGameDesc { +my ($self, $gn) = @_; + return $self->dbAll("SELECT description FROM games WHERE gamename = ?", $gn)->[0]{description}; +} + +1; diff --git a/lib/MasterWebInterface/Database/Pg/ServerInfo.pm b/lib/MasterWebInterface/Database/Pg/ServerInfo.pm new file mode 100755 index 0000000..8c6ba6f --- /dev/null +++ b/lib/MasterWebInterface/Database/Pg/ServerInfo.pm @@ -0,0 +1,178 @@ +package MasterWebInterface::Database::Pg::ServerInfo; +use strict; +use warnings; +use Exporter 'import'; +our @EXPORT = qw| dbGetServerListInfo dbGetServerDetails dbGetPlayerInfo |; + +################################################################################ +## get server details for one or multiple servers +################################################################################ +sub dbGetServerListInfo { + my $s = shift; + my %o = ( sort => '', @_ ); + + my %where = ( + $o{id} ? ( 'id = ?' => $o{id}) : (), + $o{ip} ? ( 'ip = ?' => $o{ip}) : (), + $o{port} ? ( 'port = ?' => $o{port}) : (), + $o{gamename} ? ( 'gamename = ?' => lc $o{gamename}) : (), + $o{gamever} ? ( 'gamever = ?' => $o{gamever}) : (), + $o{hostname} ? ( 'hostname = ?' => $o{hostname}) : (), + $o{hostport} ? ( 'hostport = ?' => $o{hostport}) : (), + $o{country} ? ( 'country = ?' => $o{country}) : (), + $o{b333ms} ? ( 'b333ms = ?' => $o{b333ms}) : (), + $o{blacklisted} ? ('blacklisted = ?' => $o{blacklisted}) : (), + $o{added} ? ( 'added < to_timestamp(?)' => (time-$o{added})) : (), + $o{beacon} ? ( 'beacon > to_timestamp(?)' => (time-$o{beacon})) : (), + $o{updated} ? ('updated > to_timestamp(?)' => (time-$o{updated})) : (), + $o{before} ? ('updated < to_timestamp(?)' => (time-$o{before})) : (), + ); + + my @select = ( qw| id ip port serverlist.gamename gamever hostname hostport country b333ms blacklisted description |, + "extract('epoch' from added) as e_added", + "extract('epoch' from updated) as e_updated", + "extract('epoch' from CURRENT_TIMESTAMP - added) as addiff", + "extract('epoch' from CURRENT_TIMESTAMP - updated) as updiff", + ); + + my $order = sprintf { + id => 'id %s', + ip => 'ip %s', + port => 'port %s', + gamename => 'serverlist.gamename %s', + gamever => 'gamever %s', + hostname => 'hostname %s', + hostport => 'hostport %s', + country => 'country %s', + b333ms => 'b333ms %s', + blacklisted => 'blacklisted %s', + added => 'added %s', + beacon => 'beacon %s', + updated => 'updated %s', + }->{ $o{sort}||'id' }, $o{reverse} ? 'DESC' : 'ASC'; + + return $s->dbAll( q|SELECT !s FROM serverlist + JOIN games ON serverlist.gamename = games.gamename + !W ORDER BY !s|.($o{limit} ? " LIMIT ?" : ""), + join(', ', @select), \%where, $order, ($o{limit} ? $o{limit} : ()), + ); +} + +################################################################################ +## get server details for one or multiple UT servers +################################################################################ +sub dbGetServerDetails { + my $s = shift; + my %o = (sort => '', @_ ); + + my %where = ( + $o{id} ? ('server_id = ?' => $o{id}) : (), + $o{minnetver} ? ('minnetver = ?' => $o{minnetver}) : (), + $o{location} ? ('location = ?' => $o{location}) : (), + $o{listenserver} ? ('listenserver = ?' => $o{listenserver}) : (), + $o{adminname} ? ('adminname = ?' => $o{adminname}) : (), + $o{adminemail} ? ('adminemail = ?' => $o{adminemail}) : (), + $o{password} ? ('password = ?' => $o{password}) : (), + $o{gametype} ? ('gametype = ?' => $o{gametype}) : (), + $o{gamestyle} ? ('gamestyle = ?' => $o{gamestyle}) : (), + $o{changelevels} ? ('changelevels = ?' => $o{changelevels}) : (), + $o{maptitle} ? ('maptitle = ?' => $o{maptitle}) : (), + $o{mapname} ? ('mapname = ?' => $o{mapname}) : (), + $o{numplayers} ? ('numplayers = ?' => $o{numplayers}) : (), + $o{maxplayers} ? ('maxplayers = ?' => $o{maxplayers}) : (), + $o{minplayers} ? ('minplayers = ?' => $o{minplayers}) : (), + $o{botskill} ? ('botskill = ?' => $o{botskill}) : (), + $o{balanceteams} ? ('balanceteams = ?' => $o{balanceteams}) : (), + $o{playersbalanceteams} ? ('playersbalanceteams = ?' => $o{playersbalanceteams}) : (), + $o{friendlyfire} ? ('friendlyfire = ?' => $o{friendlyfire}) : (), + $o{maxteams} ? ('maxteams = ?' => $o{maxteams}) : (), + $o{timelimit} ? ('timelimit = ?' => $o{timelimit}) : (), + $o{goalteamscore} ? ('goalteamscore = ?' => $o{goalteamscore}) : (), + $o{fraglimit} ? ('fraglimit = ?' => $o{fraglimit}) : (), + $o{mutators} ? ('mutators ILIKE ?' => "%$o{mutators}%") : (), + $o{updated} ? ('updated > to_timestamp(?)'=> (time-$o{updated})) : (), + ); + + my @select = ( qw| server_id minnetver location listenserver adminname adminemail + password gametype gamestyle changelevels maptitle mapname numplayers maxplayers + minplayers botskill balanceteams playersbalanceteams friendlyfire maxteams + timelimit goalteamscore fraglimit mutators |, + "extract('epoch' from updated) as e_updated2", + "extract('epoch' from CURRENT_TIMESTAMP - updated) as updiff2", + ); + + my $order = sprintf { + server_id => 'server_id %s', + minnetver => 'minnetver %s', + location => 'location %s', + listenserver => 'listenserver %s', + adminname => 'adminname %s', + adminemail => 'adminemail %s', + password => 'password %s', + gametype => 'gametype %s', + gamestyle => 'gamestyle %s', + changelevels => 'changelevels %s', + maptitle => 'maptitle %s', + mapname => 'mapname %s', + numplayers => 'numplayers %s', + maxplayers => 'maxplayers %s', + minplayers => 'minplayers %s', + botskill => 'botskill %s', + balanceteams => 'balanceteams %s', + playersbalanceteams => 'playersbalanceteams %s', + friendlyfire => 'friendlyfire %s', + maxteams => 'maxteams %s', + timelimit => 'timelimit %s', + goalteamscore => 'goalteamscore %s', + fraglimit => 'fraglimit %s', + mutators => 'mutators %s', + updated => 'updated %s', + }->{ $o{sort}||'server_id' }, $o{reverse} ? 'DESC' : 'ASC'; + + return $s->dbAll( q| + SELECT !s FROM extended_info + !W + ORDER BY !s| + .($o{limit} ? " LIMIT ?" : ""), + join(', ', @select), \%where, $order, ($o{limit} ? $o{limit} : ()), + ); +} + +################################################################################ +## get player details for one particular server +################################################################################ +sub dbGetPlayerInfo { + my $s = shift; + my %o = (sort => '', @_ ); + + my %where = ( + $o{server_id} ? ('server_id = ?' => $o{server_id}): (), + $o{player} ? ( 'player = ?' => $o{player}) : (), + $o{team} ? ( 'team = ?' => $o{team}) : (), + $o{frags} ? ( 'frags = ?' => $o{frags}) : (), + $o{mesh} ? ( 'mesh = ?' => $o{mesh}) : (), + $o{skin} ? ( 'skin = ?' => $o{skin}) : (), + $o{face} ? ( 'face = ?' => $o{face}) : (), + $o{ping} ? ( 'ping = ?' => $o{ping}) : (), + $o{ngsecret} ? ( 'ngsecret = ?' => $o{ngsecret}) : (), + ); + + my @select = ( qw| server_id player team frags mesh skin face ping ngsecret | ); + my $order = sprintf { + server_id => 'server_id %s', + player => 'player %s', + team => 'team %s', + frags => 'frags %s', + mesh => 'mesh %s', + skin => 'skin %s', + face => 'face %s', + ping => 'ping %s', + ngsecret => 'ngsecret %s', + }->{ $o{sort}||'team' }, $o{reverse} ? 'DESC' : 'ASC'; + + return $s->dbAll( q|SELECT !s FROM player_info !W ORDER BY !s|.($o{limit} ? " LIMIT ?" : ""), + join(', ', @select), \%where, $order, ($o{limit} ? $o{limit} : ()), + ); +} + +1; diff --git a/lib/MasterWebInterface/Database/Pg/Servers.pm b/lib/MasterWebInterface/Database/Pg/Servers.pm new file mode 100755 index 0000000..0ed47b1 --- /dev/null +++ b/lib/MasterWebInterface/Database/Pg/Servers.pm @@ -0,0 +1,91 @@ +package MasterWebInterface::Database::Pg::Servers; +use strict; +use warnings; +use Exporter 'import'; +our @EXPORT = qw| dbAddServer dbServerListGet |; + +################################################################################ +## check if an ip, port/hostport combination is recent in the serverlist. +## if not, add the address to the pending list +################################################################################ +sub dbAddServer { + my $self = shift; + my %o = (updated => 3600, @_ ); + + my %where = ( + $o{ip} ? ( 'ip = ?' => $o{ip}) : (), + $o{port} ? ( 'port = ?' => $o{port}) : (), + $o{updated} ? ('updated > to_timestamp(?)' => (time-$o{updated})) : (), + ); + + # determine if it already exsits + my $u = $self->dbAll("SELECT id FROM serverlist !W", \%where)->[0]; + return 0 if $u; + + # else, insert in pending (duplicates may exist -- see remove_pending) + $self->dbExec("INSERT INTO pending (ip, heartbeat) VALUES (?, ?)", $o{ip}, $o{port}); + return 1; +} + +################################################################################ +## get the serverlist. default 2 hours time limit +################################################################################ +sub dbServerListGet { + my $s = shift; + my %o = ( page => 1, results => 50, sort => '', updated => '7200', @_ ); + + my %where = ( + defined $o{gamename} && $o{gamename} !~ /all/ + ? ('serverlist.gamename = ?' => $o{gamename}) : (), + $o{firstchar} + ? ('upper(SUBSTRING(hostname from 1 for 1)) = ?' => $o{firstchar} ) : (), + !$o{firstchar} && defined $o{firstchar} + ? ('ASCII(hostname) < 97 OR ASCII(hostname) > 122' => 1 ) : (), + $o{search} + ? ('hostname ILIKE ?' => "%$o{search}%") : (), + $o{updated} + ? ('serverlist.updated > to_timestamp(?)' => (time-$o{updated}) ) : (), + ('length(hostname) > ?' => 1), # don't allow empty hostnames + ('hostport > ?' => 0), # or games with empty hostport + ); + + my @select = ( qw| id ip hostport hostname serverlist.gamename country numplayers maxplayers maptitle mapname gametype added description |, + "extract('epoch' from CURRENT_TIMESTAMP - serverlist.updated) as diff", + "extract('epoch' from serverlist.updated) as updated", + "extract('epoch' from serverlist.added) as added"); + + my $order = sprintf { + id => 'id %s', + ip => 'ip %s', + hostport => 'hostport %s', + hostname => 'hostname %s', + gamename => 'serverlist.gamename %s', + country => 'country %s', + diff => 'diff %s', + added => 'serverlist.added %s', + updated => 'updated %s', + gametype => 'gametype %s', + numplayers => 'numplayers %s', + maxplayers => 'maxplayers %s', + mapname => 'mapname %s', + description => 'description %s', + }->{ $o{sort}||'hostname' }, $o{reverse} ? 'DESC' : 'ASC'; + + my($r, $np) = $s->dbPage(\%o, q| + SELECT !s FROM serverlist + JOIN games ON serverlist.gamename = games.gamename + JOIN extended_info ON serverlist.id = extended_info.server_id + !W + ORDER BY !s|, + join(', ', @select), \%where, $order + ); + + my $p = $s->dbAll( q| + SELECT COUNT(*) AS num + FROM serverlist + !W|, \%where, + )->[0]{num}; + return wantarray ? ($r, $np, $p) : $r; +} + +1; diff --git a/lib/MasterWebInterface/Database/SQLite/Games.pm b/lib/MasterWebInterface/Database/SQLite/Games.pm new file mode 100755 index 0000000..db820f6 --- /dev/null +++ b/lib/MasterWebInterface/Database/SQLite/Games.pm @@ -0,0 +1,53 @@ +package MasterWebInterface::Database::SQLite::Games; +use strict; +use warnings; +use Exporter 'import'; +our @EXPORT = qw| dbGameListGet dbGetGameDesc |; + +################################################################################ +## get list of game details +## opt: filter first letter +################################################################################ +sub dbGameListGet { + my $s = shift; + my %o = (page => 1, results => 50, sort => '', @_); + + my %where = ( + $o{firstchar} + ? ('upper(SUBSTR(description, 1, 1)) = ?' => $o{firstchar} ) : (), + $o{search} + ? ('lower(description) LIKE lower(?)' => "%$o{search}%") : (), + ); + + my @select = ( qw| description gamename num_uplink num_total |); + my $order = sprintf { + description => 'description %s', + gamename => 'gamename %s', + num_uplink => 'num_uplink %s', + num_total => 'num_total %s', + }->{ $o{sort}||'num_total' }, $o{reverse} ? 'DESC' : 'ASC'; + + my($r, $np) = $s->dbPage(\%o, q| + SELECT !s FROM games + !W + ORDER BY !s|, + join(', ', @select), \%where, $order + ); + + my $p = $s->dbAll( q| + SELECT COUNT(*) AS num + FROM games + !W|, \%where, + )->[0]{num}; + return wantarray ? ($r, $np, $p) : $r; +} + +################################################################################ +## get description for a game by gamename +################################################################################ +sub dbGetGameDesc { +my ($self, $gn) = @_; + return $self->dbAll("SELECT description FROM games WHERE gamename = ?", $gn)->[0]{description}; +} + +1; diff --git a/lib/MasterWebInterface/Database/SQLite/ServerInfo.pm b/lib/MasterWebInterface/Database/SQLite/ServerInfo.pm new file mode 100755 index 0000000..31c9d57 --- /dev/null +++ b/lib/MasterWebInterface/Database/SQLite/ServerInfo.pm @@ -0,0 +1,178 @@ +package MasterWebInterface::Database::SQLite::ServerInfo; +use strict; +use warnings; +use Exporter 'import'; +our @EXPORT = qw| dbGetServerListInfo dbGetServerDetails dbGetPlayerInfo |; + +################################################################################ +## get server details for one or multiple servers +################################################################################ +sub dbGetServerListInfo { + my $s = shift; + my %o = ( sort => '', @_ ); + + my %where = ( + $o{id} ? ( 'id = ?' => $o{id}) : (), + $o{ip} ? ( 'ip = ?' => $o{ip}) : (), + $o{port} ? ( 'port = ?' => $o{port}) : (), + $o{gamename} ? ( 'gamename = ?' => lc $o{gamename}) : (), + $o{gamever} ? ( 'gamever = ?' => $o{gamever}) : (), + $o{hostname} ? ( 'hostname = ?' => $o{hostname}) : (), + $o{hostport} ? ( 'hostport = ?' => $o{hostport}) : (), + $o{country} ? ( 'country = ?' => $o{country}) : (), + $o{b333ms} ? ( 'b333ms = ?' => $o{b333ms}) : (), + $o{blacklisted} ? ('blacklisted = ?' => $o{blacklisted}) : (), + $o{added} ? ( 'added < datetime(?, \'unixepoch\')' => (time-$o{added})) : (), + $o{beacon} ? ( 'beacon > datetime(?, \'unixepoch\')' => (time-$o{beacon})) : (), + $o{updated} ? ('updated > datetime(?, \'unixepoch\')' => (time-$o{updated})) : (), + $o{before} ? ('updated < datetime(?, \'unixepoch\')' => (time-$o{before})) : (), + ); + + my @select = ( qw| id ip port serverlist.gamename gamever hostname hostport country b333ms blacklisted description |, + "strftime('\%s', added) as e_added", + "strftime('\%s', updated) as e_updated", + "strftime('\%s', CURRENT_TIMESTAMP) - strftime('\%s', added) as addiff", + "strftime('\%s', CURRENT_TIMESTAMP) - strftime('\%s', updated) as updiff", + ); + + my $order = sprintf { + id => 'id %s', + ip => 'ip %s', + port => 'port %s', + gamename => 'serverlist.gamename %s', + gamever => 'gamever %s', + hostname => 'hostname %s', + hostport => 'hostport %s', + country => 'country %s', + b333ms => 'b333ms %s', + blacklisted => 'blacklisted %s', + added => 'added %s', + beacon => 'beacon %s', + updated => 'updated %s', + }->{ $o{sort}||'id' }, $o{reverse} ? 'DESC' : 'ASC'; + + return $s->dbAll( q|SELECT !s FROM serverlist + JOIN games ON serverlist.gamename = games.gamename + !W ORDER BY !s|.($o{limit} ? " LIMIT ?" : ""), + join(', ', @select), \%where, $order, ($o{limit} ? $o{limit} : ()), + ); +} + +################################################################################ +## get server details for one or multiple UT servers +################################################################################ +sub dbGetServerDetails { + my $s = shift; + my %o = (sort => '', @_ ); + + my %where = ( + $o{id} ? ('server_id = ?' => $o{id}) : (), + $o{minnetver} ? ('minnetver = ?' => $o{minnetver}) : (), + $o{location} ? ('location = ?' => $o{location}) : (), + $o{listenserver} ? ('listenserver = ?' => $o{listenserver}) : (), + $o{adminname} ? ('adminname = ?' => $o{adminname}) : (), + $o{adminemail} ? ('adminemail = ?' => $o{adminemail}) : (), + $o{password} ? ('password = ?' => $o{password}) : (), + $o{gametype} ? ('gametype = ?' => $o{gametype}) : (), + $o{gamestyle} ? ('gamestyle = ?' => $o{gamestyle}) : (), + $o{changelevels} ? ('changelevels = ?' => $o{changelevels}) : (), + $o{maptitle} ? ('maptitle = ?' => $o{maptitle}) : (), + $o{mapname} ? ('mapname = ?' => $o{mapname}) : (), + $o{numplayers} ? ('numplayers = ?' => $o{numplayers}) : (), + $o{maxplayers} ? ('maxplayers = ?' => $o{maxplayers}) : (), + $o{minplayers} ? ('minplayers = ?' => $o{minplayers}) : (), + $o{botskill} ? ('botskill = ?' => $o{botskill}) : (), + $o{balanceteams} ? ('balanceteams = ?' => $o{balanceteams}) : (), + $o{playersbalanceteams} ? ('playersbalanceteams = ?' => $o{playersbalanceteams}) : (), + $o{friendlyfire} ? ('friendlyfire = ?' => $o{friendlyfire}) : (), + $o{maxteams} ? ('maxteams = ?' => $o{maxteams}) : (), + $o{timelimit} ? ('timelimit = ?' => $o{timelimit}) : (), + $o{goalteamscore} ? ('goalteamscore = ?' => $o{goalteamscore}) : (), + $o{fraglimit} ? ('fraglimit = ?' => $o{fraglimit}) : (), + $o{mutators} ? ('mutators ILIKE ?' => "%$o{mutators}%") : (), + $o{updated} ? ('updated > to_timestamp(?)'=> (time-$o{updated})) : (), + ); + + my @select = ( qw| server_id minnetver location listenserver adminname adminemail + password gametype gamestyle changelevels maptitle mapname numplayers maxplayers + minplayers botskill balanceteams playersbalanceteams friendlyfire maxteams + timelimit goalteamscore fraglimit mutators |, + "strftime('\%s', updated) as e_updated2", + "strftime('\%s', CURRENT_TIMESTAMP) - strftime('\%s', updated) as updiff2", + ); + + my $order = sprintf { + server_id => 'server_id %s', + minnetver => 'minnetver %s', + location => 'location %s', + listenserver => 'listenserver %s', + adminname => 'adminname %s', + adminemail => 'adminemail %s', + password => 'password %s', + gametype => 'gametype %s', + gamestyle => 'gamestyle %s', + changelevels => 'changelevels %s', + maptitle => 'maptitle %s', + mapname => 'mapname %s', + numplayers => 'numplayers %s', + maxplayers => 'maxplayers %s', + minplayers => 'minplayers %s', + botskill => 'botskill %s', + balanceteams => 'balanceteams %s', + playersbalanceteams => 'playersbalanceteams %s', + friendlyfire => 'friendlyfire %s', + maxteams => 'maxteams %s', + timelimit => 'timelimit %s', + goalteamscore => 'goalteamscore %s', + fraglimit => 'fraglimit %s', + mutators => 'mutators %s', + updated => 'updated %s', + }->{ $o{sort}||'server_id' }, $o{reverse} ? 'DESC' : 'ASC'; + + return $s->dbAll( q| + SELECT !s FROM extended_info + !W + ORDER BY !s| + .($o{limit} ? " LIMIT ?" : ""), + join(', ', @select), \%where, $order, ($o{limit} ? $o{limit} : ()), + ); +} + +################################################################################ +## get player details for one particular server +################################################################################ +sub dbGetPlayerInfo { + my $s = shift; + my %o = (sort => '', @_ ); + + my %where = ( + $o{server_id} ? ('server_id = ?' => $o{server_id}): (), + $o{player} ? ( 'player = ?' => $o{player}) : (), + $o{team} ? ( 'team = ?' => $o{team}) : (), + $o{frags} ? ( 'frags = ?' => $o{frags}) : (), + $o{mesh} ? ( 'mesh = ?' => $o{mesh}) : (), + $o{skin} ? ( 'skin = ?' => $o{skin}) : (), + $o{face} ? ( 'face = ?' => $o{face}) : (), + $o{ping} ? ( 'ping = ?' => $o{ping}) : (), + $o{ngsecret} ? ( 'ngsecret = ?' => $o{ngsecret}) : (), + ); + + my @select = ( qw| server_id player team frags mesh skin face ping ngsecret | ); + my $order = sprintf { + server_id => 'server_id %s', + player => 'player %s', + team => 'team %s', + frags => 'frags %s', + mesh => 'mesh %s', + skin => 'skin %s', + face => 'face %s', + ping => 'ping %s', + ngsecret => 'ngsecret %s', + }->{ $o{sort}||'team' }, $o{reverse} ? 'DESC' : 'ASC'; + + return $s->dbAll( q|SELECT !s FROM player_info !W ORDER BY !s|.($o{limit} ? " LIMIT ?" : ""), + join(', ', @select), \%where, $order, ($o{limit} ? $o{limit} : ()), + ); +} + +1; diff --git a/lib/MasterWebInterface/Database/SQLite/Servers.pm b/lib/MasterWebInterface/Database/SQLite/Servers.pm new file mode 100755 index 0000000..041c4d9 --- /dev/null +++ b/lib/MasterWebInterface/Database/SQLite/Servers.pm @@ -0,0 +1,89 @@ +package MasterWebInterface::Database::SQLite::Servers; +use strict; +use warnings; +use Exporter 'import'; +our @EXPORT = qw| dbAddServer dbServerListGet |; + +################################################################################ +## check if an ip, port/hostport combination is recent in the serverlist. +## if not, add the address to the pending list +################################################################################ +sub dbAddServer { + my $self = shift; + my %o = (updated => 3600, @_ ); + + my %where = ( + $o{ip} ? ( 'ip = ?' => $o{ip}) : (), + $o{port} ? ( 'port = ?' => $o{port}) : (), + $o{updated} ? ('updated > datetime(?, \'unixepoch\')' => (time-$o{updated})) : (), + ); + + # determine if it already exsits + my $u = $self->dbAll("SELECT id FROM serverlist !W", \%where)->[0]; + return 0 if $u; + + # else, insert in pending (duplicates may exist -- see remove_pending) + $self->dbExec("INSERT INTO pending (ip, heartbeat) VALUES (?, ?)", $o{ip}, $o{port}); + return 1; +} + +################################################################################ +## get the serverlist. default 2 hours time limit +################################################################################ +sub dbServerListGet { + my $s = shift; + my %o = ( page => 1, results => 50, sort => '', updated => '7200', @_ ); + + my %where = ( + defined $o{gamename} && $o{gamename} !~ /all/ + ? ('serverlist.gamename = ?' => $o{gamename}) : (), + $o{firstchar} + ? ('upper(SUBSTR(hostname, 1, 1)) = ?' => $o{firstchar} ) : (), + $o{search} + ? ('lower(hostname) LIKE lower(?)' => "%$o{search}%") : (), + $o{updated} + ? ('serverlist.updated > datetime(?, \'unixepoch\')' => (time-$o{updated})) : (), +# ('length(hostname) > ?' => 1), # don't show empty hostnames + ('hostport > ?' => 0), # or games with empty hostport + ); + + my @select = ( qw| id ip hostport hostname serverlist.gamename country numplayers maxplayers maptitle mapname gametype added description |, + "strftime('\%s', CURRENT_TIMESTAMP) - strftime('\%s', serverlist.updated) as diff", + "strftime('\%s', serverlist.updated) as updated", + "strftime('\%s', serverlist.added) as added"); + + my $order = sprintf { + id => 'id %s', + ip => 'ip %s', + hostport => 'hostport %s', + hostname => 'hostname %s', + gamename => 'serverlist.gamename %s', + country => 'country %s', + diff => 'diff %s', + added => 'serverlist.added %s', + updated => 'updated %s', + gametype => 'gametype %s', + numplayers => 'numplayers %s', + maxplayers => 'maxplayers %s', + mapname => 'mapname %s', + description => 'description %s', + }->{ $o{sort}||'hostname' }, $o{reverse} ? 'DESC' : 'ASC'; + + my($r, $np) = $s->dbPage(\%o, q| + SELECT !s FROM serverlist + JOIN games ON serverlist.gamename = games.gamename + JOIN extended_info ON serverlist.id = extended_info.server_id + !W + ORDER BY !s|, + join(', ', @select), \%where, $order + ); + + my $p = $s->dbAll( q| + SELECT COUNT(*) AS num + FROM serverlist + !W|, \%where, + )->[0]{num}; + return wantarray ? ($r, $np, $p) : $r; +} + +1; diff --git a/lib/MasterWebInterface/Handler/Extra/ExtraModules.txt b/lib/MasterWebInterface/Handler/Extra/ExtraModules.txt new file mode 100644 index 0000000..7f2de62 --- /dev/null +++ b/lib/MasterWebInterface/Handler/Extra/ExtraModules.txt @@ -0,0 +1,6 @@ +You can opt to add extra modules in this folder. See http://git.333networks.com/ for more. + +Currently available modules: + Extra/KFstats.pm - Killing Floor statistics for the UT2004 mod + Extra/JSON.pm - JSON API to allow access to the Json interface (included) + UpdateServer.pm - UpdateServer compatibility for a variety of games diff --git a/lib/MasterWebInterface/Handler/Extra/JSON.pm b/lib/MasterWebInterface/Handler/Extra/JSON.pm new file mode 100755 index 0000000..9d6c56f --- /dev/null +++ b/lib/MasterWebInterface/Handler/Extra/JSON.pm @@ -0,0 +1,404 @@ +package MasterWebInterface::Handler::Extra::JSON; +use strict; +use TUWF ':html'; +use Exporter 'import'; +use JSON; + +TUWF::register( + qr{json/(.[\w]*)} => \&serverlist_json, + qr{json/(.[\w]*)/(all|[0a-z])} => \&serverlist_json, + qr{json/(.[\w]*)/([\.\w]+):(\d+)} => \&json_serverinfo, + qr{json} => \&json_docs, +); + +################################################################################ +# LIST SERVERS +# Generate a list of selected games in the database per game (arg: gamename) +# Same as &serverlist, but with json output. +################################################################################ +sub serverlist_json { + my($self, $gamename, $char) = @_; + + # default list if nothing defined + $char = "all" unless $char; + + # process additional query information, such as order, sorting, page, etc + my $f = $self->formValidate( + { get => 'p', required => 0, default => 1, template => 'page' }, + { get => 'r', required => 0, default => 100, template => 'page' }, + { get => 'q', required => 0, default => '', maxlength => 90 }, + ); + return $self->resNotFound if $f->{_err}; + + # load server list from database + my($list, $np, $p) = $self->dbServerListGet( + reverse => $f->{o} eq 'd', + $char ne 'all' ? ( firstchar => $char ) : (), + results => $f->{r}, + search => $f->{q}, + gamename => $gamename, + page => $f->{p}, + ); + + # return json data as the response + my $json_data = encode_json [$list, {total => $p}]; + print { $self->resFd() } $json_data; + + # set content type and allow off-domain access (for example jQuery) + $self->resHeader("Access-Control-Allow-Origin", "*"); + $self->resHeader("Content-Type", "application/json; charset=UTF-8"); +} + +################################################################################ +# Server Info +# Show server info for an individual server +# Same as &server_info, but with json output. +# returns "error:1" if errors occurred +################################################################################ +sub json_serverinfo { + my ($self, $gamename, $s_addr, $s_port) = @_; + + # break address apart in valid ip, port + my ($ip,$port) = $self->valid_address($s_addr, $s_port); + + # select server from database + my $info = $self->dbGetServerListInfo( + ip => $ip, + hostport => $port, + limit => 1, + )->[0] if ($ip && $port); + + # display an error in case of an invalid IP or port + unless ($ip && $port && $info) { + my %err = (error => 1); + my $e = \%err; + my $json_data = encode_json $e; + my $json_data_size = keys %$e; + + # return json data as the response + print { $self->resFd() } $json_data; + + # set content type at the end + $self->resHeader("Access-Control-Allow-Origin", "*"); + $self->resHeader("Content-Type", "application/json; charset=UTF-8"); + return; + } + + # load additional information if available + my $details = $self->dbGetServerDetails(id => $info->{id})->[0]; + $info = { %$info, %$details } if $details; + my $json_data = encode_json $info; + my $json_data_size = keys %$info; + + # return json data as the response + print { $self->resFd() } $json_data; + + # set content type and allow off-domain access (for example jQuery) + $self->resHeader("Access-Control-Allow-Origin", "*"); + $self->resHeader("Content-Type", "application/json; charset=UTF-8"); +} + + +################################################################################ +# Json Documentation +# Minimalistic documentation about the JSON API +################################################################################ +sub json_docs { + my $self = shift; + $self->htmlHeader(title => "JSON API"); + div class => "mainbox"; + div class => "header"; + h1 "Json API"; + p class => "alttitle", "On this page you can find documentation about the 333networks masterserver JSON API."; + end; + + p "333networks has a JSON API. With this API, it is possible to obtain server lists and specific server information for your own banners, ubrowser or other application."; + + h2 "Permission & Terms of Use"; + p; + txt "In addition to our "; + a href => "/disclaimer", "Terms of Use"; + txt ", the following permissions and conditions are in effect: "; + end; + p "You are allowed to access our API with any application and/or script, self-made or not, to obtain our server lists and server information on the condition that somewhere, anywhere in your application or script you mention that the information is obtained from 333networks."; + p "You are not allowed to flood the API with requests or query our API continuously or with a short interval. If you draw too much network traffic from 333networks, we consider this flooding and will terminate your ability to query our API."; + p "Intended use: use the serverlist request to get show a list of all servers. After loading the list, your visitors/users can select a single server to display detailed information. Do NOT use the serverlist to immediately show detailed information for ALL servers, this causes a ludicrous amount of information requests and will get you excluded from our API."; + + h2 "Serverlist request"; + p "The JSON API consists of two functions to query for information. Both methods occur over HTTP and are presented as JSON data. The first method returns a list of servers and can be manipulated by gamename, first letter and number of results. 333networks applies the following regex to process your request:"; + + div class => "code"; + ul; + li "$self->{url}/json/(.[\w]*)"; + li "$self->{url}/json/(.[\w]*)/(all|[0a-z])"; + end; + end; + + p; + txt "In this regex, "; + span class => "code", "(.[\w]*)"; + txt " refers to the "; + span class => "ext", "gamename"; + txt ". This is the abbreviation that every game specifies in their masterserver protocol. A comprehensive list of gamenames is found on the "; + a href => "/g/all", "games"; + txt " page. The request can be augmented with a prefix of the "; + span class => "ext", "first letter"; + txt " of the server. For example, specifying the "; + span class => "code", "a"; + txt " will result in all server names starting with an \"a\" at the start of the name being returned."; + end; + + p; + txt "It is also possible to provide "; + span class => "code", "GET"; + txt " information in the url. Allowed options are:"; + end; + + ul; + li; span class => "code", "r"; txt " - number of results. Defaults to 50 if not specified. Minimum 1, maximum 1000."; end; + li; span class => "code", "p"; txt " - page. Show the specified page with results. Total number of entries is included in the result."; end; + li; span class => "code", "q"; txt " - search query. Identical to the search query on the "; a href => "/servers", "servers"; txt " page. Maximum query length is 90 characters."; end; + end; + + h3 "Request:"; + p; + txt "The following examples have different outcomes. In the first example, we request a serverlist of "; + span class => "code", "all"; + txt " servers, regardless of type and/or name. The second example requests only servers of the game "; + span class => "code", "Unreal"; + txt " that start with the letter "; + span class => "code", "a"; + txt ". In the last example, we request a serverlist with the gamename "; + span class => "code", "333networks"; + txt ", with only "; + span class => "code", "2"; + txt " results per page, page "; + span class => "code", "1"; + txt " and with the search word "; + span class => "code", "master"; + txt "."; + end; + + div class => "code"; + ul; + li; + txt "$self->{url}/json/"; + span class => "ext", "all"; + end; + li; + txt "$self->{url}/json/"; + span class => "ext", "unreal"; + txt "/"; + span class => "ext", "a"; + end; + li; + txt "$self->{url}/json/"; + span class => "ext", "333networks"; + txt "?r="; + span class => "ext", "2"; + txt "&p="; + span class => "ext", "1"; + txt "&q="; + span class => "ext", "master"; + end; + end; + end; + + h3 "Result:"; + p "The API returns JSON data in the following format, using the third request as an example. This is example data and may vary from what you receive when performing the same query."; + div class => "code"; + pre '[ + [ + { + "gametype":"MasterServer", + "description":"333networks MasterServer (Synchronization Protocol)", + "hostport":28905, + "updated":"1506087218", + "hostname":"dev.333networks.com (333networks Development MasterServer)", + "maxplayers":2965, + "country":"NL", + "mapname":"333networks", + "added":"1500485970.98186", + "numplayers":20, + "gamename":"333networks", + "diff":"82", + "id":869, + "ip":"84.83.176.234", + "maptitle":null + }, + { + "diff":"102", + "id":870, + "gamename":"333networks", + "numplayers":21, + "added":"1500485971.17096", + "maptitle":null,"ip":"84.83.176.234", + "hostname":"master.333networks.com (333networks Main MasterServer)", + "updated":"1506087198", + "description":"333networks MasterServer (Synchronization Protocol)", + "hostport":28900, + "gametype":"MasterServer", + "mapname":null,"country":"NL", + "maxplayers":2965 + } + ], + {"total":"3"} +]'; + end; + + p; + txt "The result contains an array of server entries and the "; + span class => "code", "total"; + txt " amount of entries. In this case, that is "; + span class => "code", "2"; + txt " entries listed and "; + span class => "code", "3"; + txt " total entries, implying that there is one more server not shown or on a next page. With the specified number of results specified by the user and the total amount of servers provided by the API, you can calculate how many pages there are to be specified. Every server entry has a number of unsorted keywords. The available keywords are:"; + end; + + ul; + li; span class => "code", "id"; txt " - server ID in our database"; end; + li; span class => "code", "ip"; txt " - IPv4 address"; end; + li; span class => "code", "hostport"; txt " - hostport to join the game. This port is also used to query specific server information (read more below)"; end; + li; span class => "code", "hostname"; txt " - name of the server"; end; + li; span class => "code", "gamename"; txt " - gamename of the server"; end; + li; span class => "code", "description"; txt " - gamename of the server as comprehensible game title"; end; + li; span class => "code", "country"; txt " - 2-letter country code where the server is hosted"; end; + li; span class => "code", "numplayers"; txt " - current number of players"; end; + li; span class => "code", "maxplayers"; txt " - maximum number of players"; end; + li; span class => "code", "mapname"; txt " - filename of current map"; end; + li; span class => "code", "maptitle"; txt " - title or description of current map"; end; + li; span class => "code", "gametype"; txt " - type of game: capture the flag, deathmatch, etc"; end; + li; span class => "code", "added"; txt " - date that the server was added to our database"; end; + li; span class => "code", "updated"; txt " - date that the server was updated in our database"; end; + li; span class => "code", "diff"; txt " - amount of seconds since this server was updated in our database"; end; + end; + p "There are more keywords available for the individual servers. Detailed information about a server is obtained with the Server Information request as described below."; + + h2 "Server Information request"; + p "Your application or script can also request detailed information for a single server. This is done in a similar way as requesting a server list. The following general regex is used by 333networks:"; + div class => "code"; + ul; + li "$self->{url}/json/(.[\w]*)/([\.\w]+):(\d+)"; + end; + end; + + p; + txt "This restricts requests to the correct url with a "; + span class => "code", "gamename"; + txt ", an "; + span class => "ext", "IP address"; + txt " or "; + span class => "ext", "domain name"; + txt " and a "; + span class => "ext", "decimal number"; + txt ". There are no additional query options or GET options. It is possible that the gamename specified does not match the "; + txt "gamename"; + txt " as stored in our database. The result will include the gamename that was specified in our database."; + end; + + p "The following two examples both request detailed information by IP address and domain name."; + h3 "Request:"; + div class => "code"; + ul; + li; + txt "$self->{url}/json/"; + span class => "ext", "333networks"; + txt "/"; + span class => "ext", "84.83.176.234"; + txt ":"; + span class => "ext", "28900"; + end; + li; + txt "$self->{url}/json/"; + span class => "ext", "333networks"; + txt "/"; + span class => "ext", "master.333networks.com"; + txt ":"; + span class => "ext", "28900"; + end; + end; + end; + + h3 "Result:"; + p "The API returns JSON data in the following format, using the requests above as an example. This is example data and may vary from what you receive when performing the same query."; + div class => "code"; + pre '{ + "beacon":"2017-09-22 16:49:18+02", + "updated":"2017-09-22 16:49:18+02", + "numplayers":21, + "country":"NL", + "hostport":28900, + "added":"2017-07-19 19:39:31.170957+02", + "maxplayers":2965, + "gamename":"333networks", + "gamever":"MS-perl 2.3.1", + "hostname":"master.333networks.com (333networks Main MasterServer)", + "friendlyfire":null, + "listenserver":null, + "updiff":"48", + "adminname":"Darkelarious", + "minplayers":0, + "mutators":"333networks synchronization, master applet synchronization", + "mapname":null, + "maxteams":null, + "fraglimit":null, + "blacklisted":0, + "playersbalanceteams":null, + "ip":"84.83.176.234", + "minnetver":null, + "maptitle":null, + "port":27900, + "password":null, + "b333ms":1, + "botskill":null, + "server_id":870, + "adminemail":"info@333networks.com", + "id":870, + "gametype":"MasterServer", + "gamestyle":null, + "balanceteams":null, + "changelevels":null, + "goalteamscore":null, + "timelimit":null, + "location":null, +}'; + end; + p "The result has a single entry of parameters with a number of unsorted keywords. The available keywords are in addition to the keywords specified in the serverlist:"; + ul; + li; span class => "code", "server_id"; txt " - detailed server ID in our database"; end; + li; span class => "code", "minnetver"; txt " - minimal required game version to join"; end; + li; span class => "code", "location"; txt " - geographical area (GameSpy)"; end; + li; span class => "code", "listenserver"; txt " - dedicated server?"; end; + li; span class => "code", "adminname"; txt " - server administrator's name"; end; + li; span class => "code", "adminemail"; txt " - server administrator's contact information"; end; + li; span class => "code", "password"; txt " - passworded/locked server"; end; + li; span class => "code", "gamestyle"; txt " - in-game playing style"; end; + li; span class => "code", "changelevels"; txt " - automatically change levels after match end"; end; + li; span class => "code", "minplayers"; txt " - number of bots"; end; + li; span class => "code", "botskill"; txt " - skill level of bots"; end; + li; span class => "code", "balanceteams"; txt " - team balancing on join"; end; + li; span class => "code", "playersbalanceteams"; txt " - players can toggle automatic team balancing"; end; + li; span class => "code", "friendlyfire"; txt " - friendly fire rate"; end; + li; span class => "code", "maxteams"; txt " - maximum number of teams"; end; + li; span class => "code", "timelimit"; txt " - time limit per match"; end; + li; span class => "code", "goalteamscore"; txt " - score limit per match"; end; + li; span class => "code", "fraglimit"; txt " - score limit per deathmatch"; end; + li; span class => "code", "mutators"; txt " - comma-separated mutator/mod list"; end; + li; span class => "code", "b333ms"; txt " - direct beacon to the masterserver"; end; + li; span class => "code", "beacon"; txt " - date that the last beacon was received"; end; + li; span class => "code", "blacklisted"; txt " - server is blacklisted at 333networks"; end; + end; + + h2 "Feedback"; + p; + txt "We wrote the JSON API with the intention to make the 333networks masterserver data as accessible as possible. If you feel like any functionality is missing or incorrectly shared, do not hesitate to "; + a href => "/contact", "contact"; + txt " us and to provide feedback. Additionally, we request that you follow the advise on usage as we described under the Terms of Use on top of this page, so we can keep providing this API."; + end; + + end; # mainbox + $self->htmlFooter(last_change => "Sep 2017"); +} + +1; diff --git a/lib/MasterWebInterface/Handler/FrontPage.pm b/lib/MasterWebInterface/Handler/FrontPage.pm new file mode 100755 index 0000000..f57c8e1 --- /dev/null +++ b/lib/MasterWebInterface/Handler/FrontPage.pm @@ -0,0 +1,220 @@ +package MasterWebInterface::Handler::FrontPage; +use strict; +use warnings; +use utf8; +use TUWF ':html'; +use Exporter 'import'; +our @EXPORT = qw| _box_content |; + +TUWF::register( + qr{} => \&main, +); + +################################################################################ +# Front page +# Contains list of new servers and boxes with information and stats. +################################################################################ +sub main { + my ($self, @args) = @_; + + # workaround for lists -- record options, but don't use them. + my $f = $self->formValidate( + { get => 's', required => 0, default => '', enum => [ ] }, + { get => 'o', required => 0, default => 'd', enum => [ 'a','d' ] }, + { get => 'p', required => 0, default => 1, template => 'page' }, + { get => 'q', required => 0, default => '', maxlength => 30 }, + ); + + $self->htmlHeader(title => "Welcome"); + + # load NEW SERVERS list from database + my ($list, $np, $p) = $self->dbServerListGet(results => 8, sort => 'added', reverse => 1); + my $odd = 0; + + # print list of new servers added to the database + $self->htmlBrowse( + items => $list, + options => $f, + total => $p, + nextpage => 0, + pageurl => "/s/all/all", + sorturl => "/s/all/all", + class => "newservers", + footer => sub { + Tr ++$odd % 2 ? (class => 'even') : (class => 'odd'), id => "tfooter"; + td colspan => 4; + txt "Add your server "; + a href => '/new', 'here'; + txt "!"; + end; + end 'tr';}, + ($p <= 0) ? (footer => sub {Tr;td colspan => 4, class => 'tc2', 'No recent servers found';end 'tr';}) : (), + header => [ + [ '', 'country' ], + [ 'Newest servers', 'hostname'], + [ 'Game', 'description'], + [ 'Added', 'added' ], + ], + row => sub { + my($s, $n, $l) = @_; + Tr $n % 2 ? (class => 's odd') : (class => 's'); + my ($flag, $country) = $self->countryflag($l->{country}); + td class => "tc1 flag", style => "background-image: url(/flag/$flag.svg);", title => $country, ''; + td class => "tc2"; a href => "/$l->{gamename}/$l->{ip}:$l->{hostport}", $l->{hostname}; end; + td class => "tc3"; a href => "/s/$l->{gamename}", $l->{description};end; + td $self->date_new($l->{added}); + end; + $odd = $n; # for footer + }, + ); + + # opening and welcome + div class => "mainbox"; + div class => "header"; + h2 "Welcome to $self->{site_name}"; + end; + p "On this website, you find a plain overview of all server addresses that are listed in our masterserver and all games that are currently supported. On this website you can also find links to instructions to add your online server to the masterserver, and how to receive the list from our masterserver as game player."; + end; + br style => "clear:both"; + +# div class => 'notice'; +# h2 "Generic Title"; +# p "Generic paragraph."; +# end; + + # + # two-sided pane with multiple boxes + div class => "frontcontainer"; + div class => "frontleft"; + $self->_box_content("populargames", $f); + $self->_box_content("errorist"); + end; + + div class => "frontright"; + $self->_box_content("onlinemasters", $f); + $self->_box_content("instructions"); + end; + end; + br style => "clear:both"; + + $self->htmlFooter(); +} + + +################################################################################ +## Content Boxes for front page +## (not in a specific order) +################################################################################ +sub _box_content { + my ($self, $k, $f) = @_; + + # + # Online Masterservers + if ($k eq 'onlinemasters') { + # load server list from database + my ($list, $np, $p) = $self->dbServerListGet( + results => 15, + sort => "hostname", + reverse => 0, + gamename => "333networks", + updated => 1800 + ); + + # print list + $self->htmlBrowse( + items => $list, + options => $f, + total => $p, + nextpage => 0, + pageurl => "/s/all/all", + sorturl => "/s/all/all", + class => "frontmasterlist", + ($p <= 0) ? (footer => sub {Tr;td colspan => 3, class => 'tc2', 'No masterservers found!';end 'tr';}) : (), + header => [ + [ '', 'country' ], + [ 'Masterserver Address', 'hostname'], + [ 'Last seen', 'updated' ], + ], + row => sub { + my($s, $n, $l) = @_; + Tr $n % 2 ? (class => 's odd') : (class => 's'); + my ($flag, $country) = $self->countryflag($l->{country}); + td class => "tc1 flag", style => "background-image: url(/flag/$flag.svg);", title => $country, ''; + td class => "tc2"; a href => "/$l->{gamename}/$l->{ip}:$l->{hostport}", (split(' ', $l->{hostname}))[0]; end; + td $self->timeformat($l->{diff}); + end; + }, + ); + return; + } + + # + # Instructions + if ($k eq 'instructions') { + div class => "mainbox"; + $self->figurelink("masterserver", "ubrowser2.jpg", "http://333networks.com/instructions"); + h2 "Instructions"; + p; + txt "In order to make online games work again after GameSpy ceased all services a lot of online multiplayer games were no longer supported. 333networks provides an alternative masterserver. This masterserver needs to be "; + span class => "ext", "manually"; + txt " activated in your settings. This can be done by adding one of the following masterserver addresses to your client settings:"; + end; + ul; + li "master.333networks.com:28900"; + li "master.errorist.tk:28900"; + #li "master.noccer.de:28900"; + li "master.newbiesplayground.net:28900"; + end; + p "As player, you can configure your game in the following way: find your configuration file and update your masterserver entries."; + p;a href => "http://333networks.com/instructions", "[Read the quick instruction here]";end; + end; + return; + } + + # + # Popular games + if ($k eq 'populargames') { + # load server list from database + my($list, $np, $p) = $self->dbGameListGet(results => 15, sort => 'num_total', reverse => 1); + # print list + $self->htmlBrowse( + items => $list, + options => $f, + total => $p, + nextpage => 0, + pageurl => "/g/all", + sorturl => "/g/all", + class => "frontpage", + ($p <= 0) ? (footer => sub {Tr;td colspan => 3, class => 'tc1', 'No games found.';end 'tr';}) : (), + header => [ + [ 'Top 10 popular games', 'description' ], + [ 'Direct', 'num_uplink' ], + [ 'Total', 'num_total' ], + ], + row => sub { + my($s, $n, $l) = @_; + Tr $n % 2 ? (class => 's odd') : (class => 's'); + td class => "tc1 flag"; a href => "/s/$l->{gamename}", $l->{description};end; + td $l->{num_uplink}; + td $l->{num_total}; + end; + }, + ); + return; + } + + # + # Errorist Forum + if ($k eq 'errorist') { + div class => "mainbox"; + $self->figurelink("other", "erroristforum.jpg", "http://forum.errorist.tk"); + h2 "The Errorist Network"; + p "Together with Errorist, we started and share our own forum. This platform is a development corner for UEngine games and the 333networks masterserver + games using it. Visit us at forum.errorist.tk and sign up!"; + p; + a href => "http://forum.errorist.tk", "[Join the talks!]"; + end; + end; + return; + } +} +1; diff --git a/lib/MasterWebInterface/Handler/Games.pm b/lib/MasterWebInterface/Handler/Games.pm new file mode 100755 index 0000000..54837eb --- /dev/null +++ b/lib/MasterWebInterface/Handler/Games.pm @@ -0,0 +1,97 @@ +package MasterWebInterface::Handler::Games; +use strict; +use TUWF ':html'; +use Exporter 'import'; +use Geography::Countries; + +TUWF::register( + qr{g} => \&gamelist, + qr{g/} => \&gamelist, + qr{g/(all|[a-z])} => \&gamelist, + qr{g/(.[\w]*)} => \&redirect_game, +); + +# redirect to /s/gamename (compatibility with old urls -- remove eventually) +sub redirect_game { + my ($self, $g) = @_; + return $self->resRedirect("/s/$g"); +} + +################################################################################ +# LIST GAMES +# Generate a list of games in the database (arg: gamename) +################################################################################ +sub gamelist { + my($self, $char) = @_; + + # default list if nothing defined + $char = "all" unless $char; + + # process additional query information, such as order, sorting, page, etc + my $f = $self->formValidate( + { get => 's', required => 0, default => 'num_total', enum => [ qw| description gamename num_uplink num_total | ] }, + { get => 'o', required => 0, default => 'd', enum => [ 'a','d' ] }, + { get => 'p', required => 0, default => 1, template => 'page' }, + { get => 'q', required => 0, default => '', maxlength => 30 }, + ); + return $self->resNotFound if $f->{_err}; + + # load server list from database + my($list, $np, $p) = $self->dbGameListGet( + sort => $f->{s}, + reverse => $f->{o} eq 'd', + $char ne 'all' ? ( + firstchar => uc $char ) : (), + results => 50, + search => $f->{q}, + page => $f->{p}, + ); + + $self->htmlHeader(title => "Browse Games"); + + div class => 'mainbox'; + div class => "header"; + h1 'Browse Games'; + p class => "alttitle", "An overview of all registered games, direct uplinks to our masterserver and the total amount of servers seen."; + end; + + form action => "/g/$char", 'accept-charset' => 'UTF-8', method => 'get'; + $self->htmlSearchBox('g', $f->{q}); + end; + p class => 'browseopts'; + for ('all', 'a'..'z') { + a href => "/g/$_", $_ eq $char ? (class => 'optselected') : (), $_ eq 'all' ? ('all') : $_ ? uc $_ : '#'; + } + end; + end; + + # print list + $self->htmlBrowse( + items => $list, + options => $f, + total => $p, + nextpage => [$p,50],#$np, + pageurl => "/g/$char?o=$f->{o};s=$f->{s};q=$f->{q}", + sorturl => "/g/$char?q=$f->{q}", + class => "gamelist", + ($p <= 0) ? (footer => sub {Tr;td colspan => 4, class => 'tc2', 'No games found.';end 'tr';}) : (), + header => [ + [ 'Game', 'description' ], + [ 'Code', 'gamename' ], + [ 'Direct', 'num_uplink' ], + [ 'Total', 'num_total' ], + ], + row => sub { + my($s, $n, $l) = @_; + Tr $n % 2 ? (class => 's odd') : (class => 's'); + td class => "tc1"; a href => "/s/$l->{gamename}", $l->{description};end; + td $l->{gamename}; + td $l->{num_uplink}; + td $l->{num_total}; + end; + }, + ); + + $self->htmlFooter; +} +1; diff --git a/lib/MasterWebInterface/Handler/ServInfo.pm b/lib/MasterWebInterface/Handler/ServInfo.pm new file mode 100755 index 0000000..e0aff47 --- /dev/null +++ b/lib/MasterWebInterface/Handler/ServInfo.pm @@ -0,0 +1,438 @@ +package MasterWebInterface::Handler::ServInfo; +use strict; +use warnings; +use utf8; +use TUWF ':html'; +use URI::Escape; +use POSIX 'strftime'; +use Exporter 'import'; +our @EXPORT = qw| serverError |; + +TUWF::register( + qr{(.[\w]*)/([\.\w]+):(\d+)} => \&show_server, +); + +################################################################################ +# Display server information +# Verify if game and server (ip:hostport) exist. Display as many available +# values as possible. +# Display error pages if not found or incorrect. +################################################################################ +sub show_server { + # self, gamename, "ip:port" + my ($self, $gamename, $s_addr, $s_port) = @_; + + # break address apart in valid ip, port + my ($ip,$port) = $self->valid_address($s_addr, $s_port); + + # display an error in case of an invalid IP or port + unless ($ip && $port) { + $self->serverError( + ip => ($ip ? 0 : 1), + port => ($port ? 0 : 1), + ); + return; + } + + # select server from database + my $info = $self->dbGetServerListInfo( + ip => $ip, + hostport => $port, + limit => 1, + )->[0]; + + # either redirect or show error when no info was found + if (!defined $info) { + # try if query port was provided instead + my $attempt = $self->dbGetServerListInfo( + ip => $ip, + port => $port, + limit => 1 + )->[0]; + + # if it exists now, automatically redirect to this page (don't show info here) + if (defined $attempt && defined $attempt->{hostport}) { + $self->resRedirect("/$gamename/$ip:$attempt->{hostport}"); + return; + } + + # otherwise, it was not found in the database at all. Notify. + $self->serverError(db => 1); + return; + } + + # load additional information if available + my $details = $self->dbGetServerDetails(id => $info->{id})->[0]; + $info = { %$info, %$details } if $details; + + # + # generate info page + # + $self->htmlHeader(title => $info->{hostname} || "Yet Another Server"); + div class => "mainbox detail"; + div class => "header"; + h1 $info->{hostname} || "Yet Another Server"; + end; + + # if no detailed info was found, the server was not updated or + # the game is not supported. + if (!defined $details) { + div class => 'warning'; + h2 'Detailed information missing!'; + p "Additional information could not be loaded. Either the server was not updated in our database, or detailed information for this game is not yet supported. The information on this page may not be accurate!"; + end; + } + + # + # Map thumbnail and bot info + # + div class => "container"; + div class => "thumbnail"; + + # find the correct thumbnail, otherwise standard 333 esrb pic + my $mapfig = "$self->{map_url}/default/333esrb.jpg"; + + # get prefix and mapname + my $mapname = lc $info->{mapname}; + my ($pre,$post) = $mapname =~ /^(DM|CTF\-BT|BT|CTF|DOM|AS|JB|TO|SCR|MH)-(.*)/i; + my $prefix = ($pre ? uc $pre : "other"); + + # if map figure exists, use it + if (-e "$self->{map_dir}/$info->{gamename}/$prefix/$mapname.jpg") { + $mapfig = "$self->{map_url}/$info->{gamename}/$prefix/$mapname.jpg"; + } + + # if not, game default image + elsif (-e "$self->{map_dir}/default/$info->{gamename}.jpg") { + $mapfig = "$self->{map_url}/default/$info->{gamename}.jpg"; + } + + img src => $mapfig, + alt => $mapfig, + title => ($info->{mapname} || "Unknown"); + span ($info->{maptitle} || ($info->{mapname} || "Unknown")); + end; + + table class => "mapinfo"; + if ($info->{maxplayers}) { + Tr; + td class => "wc1", "Players:"; + td; + txt $info->{numplayers} || 0; + txt "/"; + txt $info->{maxplayers} || 0; + end; + end; + } + if ($info->{botskill} && $info->{minplayers}) { + Tr; + td "Bots:"; + td; + txt $info->{minplayers} || 0; + txt " "; + txt ($info->{botskill} || "Standard"); + txt " bot"; txt ($info->{minplayers} == 1 ? "" : "s"); + end; + end; + } + end; + end; # container + + # + # Specific server entry information + # + table class => "serverinfo"; + Tr; + th class => "wc1", title => "Server ID: ".$info->{id}, "Server Info"; + th ""; + end; + Tr; + td "Address:"; + td title => $info->{port}, (($info->{ip} || $ip). ":". ($info->{hostport} || $port)); + end; + if ($info->{adminname}) { + Tr; + td "Admin:"; + td $info->{adminname}; + end; + } + Tr; + td class => "wc1", "Contact:"; + td; + if ($info->{adminemail}) {txt $info->{adminemail}} else { + i; + txt "This server has no contact information listed "; + a href => "https://ut99.org/viewtopic.php?f=33&t=6660", "[?]"; + end; + } + end; + end; + Tr; + td class => "wc1", "Location:"; + my ($flag, $country) = $self->countryflag($info->{country} || ""); + td; + img class => "flag", src => "/flag/$flag.svg"; + txt " ". $country; + end; + end; + Tr; { + td "Added:"; + my @t = gmtime($info->{addiff}); + my $sig = 0; + my $diff = ""; + if ($t[5]-70){$diff.=$t[5]-70 ." year".(($t[5]-70==1)?"":"s"); $sig++} + if ($t[7]){$diff.=($sig?", ":""). $t[7]." day".(($t[7]==1)?"":"s")} + if ($diff eq "") {$diff = "Less than one day";} + td $diff." ago (".(strftime "%e %b %Y", gmtime $info->{e_added}).")";} + end; + Tr; + td "Last seen:"; + td;{ + my @t = gmtime($info->{updiff}); + if ($t[5]-70 || $t[7]) { + # more than 1 day? show date + span class => "r", (strftime "%e %b %Y", gmtime $info->{e_updated}); + } else { + # less than 1 day? show "time ago" + my $diff = ""; + $diff .= ($t[2] ? $t[2]." hour".($t[2]>1?"s, ":", ") : ""); + $diff .= ($t[1] ? $t[1]." minute".($t[1]>1?"s, ":", ") : ""); + $diff .= ($t[0] ? $t[0]." second".($t[0]>1?"s":" ") : "0 seconds"); + $diff .= " ago"; + span $diff; + } + end;} + end; + Tr; + td "Flags: "; + td; + i ($info->{b333ms} ? "direct uplink, " : "applet or manual, "); + i ($info->{blacklisted} ? "blacklisted, " : "not blacklisted, "); + i ($info->{password} ? "passworded" : "not passworded"); + end; + end; + end; + + # + # Specific game and version information + # + table class => "gameinfo"; + Tr; + th class => "wc1", "Game Info"; + th ""; + end; + Tr; + td "Game:"; + td $info->{description} || $gamename; + end; + if ($info->{gametype}) { + Tr; + td "Type:"; + td $info->{gametype}; + end; + } + if ($info->{gamestyle}) { + Tr; + td "Style:"; + td $info->{gamestyle}; + end; + } + if ($info->{gamever}) { + Tr; + td "Version:"; + td $info->{gamever}; + end; + } + end; + + # + # Mutator list + # + table class => "mutators"; + Tr; + th "Mutators"; + end; + Tr; + td; + if (defined $info->{mutators} && $info->{mutators} ne "None") { + txt $info->{mutators};} + else {i "This server does not have any mutators listed.";} + end; + end; + end; + + table class => "players"; + my $player = $self->dbGetPlayerInfo(server_id => $info->{id}); + my %team = ( 0 => "#e66", + 1 => "#66e", + 2 => "#6e6", + 3 => "#ee6", + 4 => "#fe6" ); + + # loop through players and print them in a nicely formatted table with funky colors + Tr; + th class => "wc1", "Player Info"; + th class => "frags", 'Frags'; + th class => "mesh", 'Mesh'; + th class => "skin", 'Skin'; + th class => "ping", 'Ping'; + end; + + for (my $i=0; defined $player->[$i]->{player}; $i++) { + Tr $i % 2 ? (class => 'odd') : (), style => 'color:'.$team{$player->[$i]->{team}} || "#aaa"; + td class => "wc1", $player->[$i]->{player} . (($player->[$i]->{ngsecret} =~ m/^bot$/i) ? " (bot)" : ""); + td class => "frags", $player->[$i]->{frags}; + td class => "mesh", $player->[$i]->{mesh}; + td class => "skin", $player->[$i]->{skin}; + td class => "ping", $player->[$i]->{ping}; + end; + } + if (!defined $player->[0]->{player}) { Tr; td colspan => 5; lit '<i>There is no player information available.</i>'; end; end;} + end; + + + + # + # Share options (copy fields) + # + my $url = $self->{url}. "/". $gamename. "/". $info->{ip}. ":". $info->{hostport}; + table class => "shareopts"; + Tr; + th class => "wc1", "Share"; + th ""; + end; + Tr; + td class => "tc1", "Link"; + td class => "tc2"; + input type => 'text', class => 'text', name => 'url', value => $url; + end; + end; + Tr; + td class => "tc1"; + txt "Json API "; + a href => "/json", title => "The url to access this server over the 333networks Json API", "*"; + end; + td class => "tc2"; + input type => 'text', class => 'text', name => 'url', value => $self->{url}. "/json/". $gamename. "/". $info->{ip}. ":". $info->{hostport}; + end; + end; + Tr; + td "Forum Link"; + td; + textarea type => 'textarea', class => 'text', rows => 3, name => 'paste'; + txt "\[url=$url\]";lit "\n"; + lit "\t";txt $info->{hostname};lit "\n"; + txt "\[/url\]"; + end; + end; + end; + Tr; + td "HTML Code"; + td; + textarea type => 'textarea', class => 'text', rows => 3, name => 'paste'; + txt "<a href=\"$url\">";lit "\n"; + lit "\t"; txt $info->{hostname};lit "\n"; + txt "</a>"; + end; + end; + end; + end; + +=pod + Optional information blocks: + + # + # Teams + # + table class => "teaminfo"; + Tr; + th class => "wc1", "Team Info"; + th ""; + end; + Tr; + td "Balance Teams:"; + td ($info->{balanceteams} ? "Yes" : "No"); + end; + Tr; + td "Players Balance Teams:"; + td ($info->{playersbalanceteams} ? "Yes" : "No"); + end; + Tr; + td "Friendly Fire:"; + td ($info->{friendlyfire} || "0%"); + end; + Tr; + td "Max Teams:"; + td ($info->{maxteams} || 1); + end; + end; + + # + # Game Limits + # + table class => "limits"; + Tr; + th class => "wc1", "Limits"; + th ""; + end; + Tr; + td "Time Limit:"; + td (($info->{timelimit} || 0). ":00 min"); + end; + Tr; + td "Score Limit:"; + td ($info->{goalteamscore} || 0); + end; + Tr; + td "Frag Limit:"; + td ($info->{fraglimit} || 0); + end; + end; + + div class => "code"; + use Data::Dumper 'Dumper'; + pre; + txt Dumper [$info, $player]; + end; + end; + + if ($self->debug) { + use Data::Dumper 'Dumper'; + lit "<!--\n"; + lit Dumper $info; + lit Dumper $player; + lit "\n-->"; + } + +=cut + end; # mainbox details + $self->htmlFooter; +} + +################################################################################ +# Display server errors +# Generates error pages in case of faulty gamename, server or other vagueness +################################################################################ +sub serverError{ + my ($self, %error) = @_; + + $self->htmlHeader(title => "Server Info"); + div class => 'warning'; + h2 'An error occurred while trying to display the server.'; + ul; + if (!%error) { + li "Not even the error message works. Please contact the administrator.";} + if ($error{ip}) { + li "The provided address is incorrect or does not resolve.";} + if ($error{port}) { + li "The provided port is not valid.";} + if ($error{gamename}) { + li "The game was not found in our database.";} + if ($error{db}) { + li "The server was not found in our database.";} + end; + end; + $self->htmlFooter; +} + +1; diff --git a/lib/MasterWebInterface/Handler/Servers.pm b/lib/MasterWebInterface/Handler/Servers.pm new file mode 100755 index 0000000..306a2c8 --- /dev/null +++ b/lib/MasterWebInterface/Handler/Servers.pm @@ -0,0 +1,105 @@ +package MasterWebInterface::Handler::Servers; +use strict; +use utf8; +use TUWF ':html'; +use Exporter 'import'; +use Geography::Countries; + +TUWF::register( + qr{s/(.[\w]*)} => \&serverlist, + qr{s/(.[\w]*)/(all|[0a-z])} => \&serverlist, +); + +################################################################################ +# List servers +# Generate a list of selected games in the database per game (arg: gamename) +################################################################################ +sub serverlist { + my($self, $gamename, $char) = @_; + + # default list if nothing defined + $char = "all" unless $char; + + # process additional query information, such as order, sorting, page, etc + my $f = $self->formValidate( + { get => 's', required => 0, default => 'diff', enum => [ qw| country hostname description gamename gametype ip hostport numplayers mapname diff added | ] }, + { get => 'o', required => 0, default => 'a', enum => [ 'a','d' ] }, + { get => 'p', required => 0, default => 1, template => 'page' }, + { get => 'q', required => 0, default => '', maxlength => 90 }, + ); + return $self->resNotFound if $f->{_err}; + + # load server list from database + my($list, $np, $p) = $self->dbServerListGet( + sort => $f->{s}, + reverse => $f->{o} eq 'd', + $char ne 'all' ? ( + firstchar => uc $char ) : (), + results => 50, + search => $f->{q}, + gamename => $gamename, + page => $f->{p}, + ); + + # game name description in title + my $gn_desc = $self->dbGetGameDesc($gamename) || $gamename; + + # Write page + $self->htmlHeader(title => "Browse $gn_desc game servers"); + + div class => 'mainbox'; + div class => "header"; + h1 'Browse Servers'; + p class => "alttitle"; + txt "Servers listed for "; + span class => "acc", $gn_desc; + txt " games. Can be sorted by location, server name, gametype, players and current map."; + end; + end; + + form action => "/s/$gamename/all", 'accept-charset' => 'UTF-8', method => 'get'; + $self->htmlSearchBox('s', $f->{q}); + end; + p class => 'browseopts'; + for ('all', 'a'..'z', 0) { + a href => "/s/$gamename/$_", $_ eq $char ? (class => 'optselected') : (), $_ eq 'all' ? ('all') : $_ ? uc $_ : '#'; + } + end; + end; + + # print list + $self->htmlBrowse( + items => $list, + options => $f, + total => $p, + nextpage => [$p,50],#$np, + pageurl => "/s/$gamename/$char?o=$f->{o};s=$f->{s};q=$f->{q}", + sorturl => "/s/$gamename/$char?q=$f->{q}", + class => "serverlist", + ($p <= 0) ? (footer => sub {Tr;td colspan => 6, class => 'tc2', 'No online servers found';end 'tr';}) : (), + header => [ + [ '', 'country' ], + [ 'Server Name', 'hostname' ], + [ 'Game', 'gamename' ], + [ 'Gametype', 'gametype' ], + [ 'Players', 'numplayers' ], + [ 'Map', 'mapname' ], + ], + row => sub { + my($s, $n, $l) = @_; + Tr $n % 2 ? (class => 's odd') : (class => 's'); + my ($flag, $country) = $self->countryflag($l->{country}); + td class => "tc1 flag", style => "background-image: url(/flag/$flag.svg);", title => $country, ''; + td class => "tc2"; a href => "/$l->{gamename}/$l->{ip}:$l->{hostport}", $l->{hostname}; end; + td class => "tc3", title => $l->{description}; a href => "/s/$l->{gamename}", $l->{gamename};end; + td class => "tc4", title => $l->{gametype}, $l->{gametype}; + td class => "tc5"; txt $l->{numplayers}; txt "/"; txt $l->{maxplayers}; end; + td class => "tc6", title => ( $l->{maptitle} || $l->{mapname}), ($l->{maptitle} || $l->{mapname}); + end; + }, + ); + + $self->htmlFooter; +} + +1; diff --git a/lib/MasterWebInterface/Handler/Static/ErrorPages.pm b/lib/MasterWebInterface/Handler/Static/ErrorPages.pm new file mode 100755 index 0000000..02dfd15 --- /dev/null +++ b/lib/MasterWebInterface/Handler/Static/ErrorPages.pm @@ -0,0 +1,141 @@ +package MasterWebInterface::Handler::Static::ErrorPages; +use strict; +use TUWF ':html'; + +################################################################################ +# TUWF: +# Set the default error pages for errors +################################################################################ +TUWF::set( + error_400_handler => \&handle400, + error_404_handler => \&handle404, + error_405_handler => \&handle405, + error_413_handler => \&handle413, + error_500_handler => \&handle500, +); + +TUWF::register( + qr{500} => sub {die "Process died on purpose"}, + qr{unavailable} => \&handle_unavailable, + qr{nospam} => \&nospam, +); + +################################################################################ +# Catch malformed links that were not replaced by the javascript tool +# Can also be the result of no javascript. +################################################################################ +sub nospam { + my $self = shift; + $self->htmlHeader(title => 'Go Away!', noindex => 1); + + div class => 'warning'; + h1 'Form Error'; + p 'The form could not be sent. Either you are a robot or you do not have Javascript enabled in your browser.'; + end; + + $self->htmlFooter; +} + +################################################################################ +# Not yet available +################################################################################ +sub handle_unavailable { + my $self = shift; + + $self->htmlHeader(title => 'Function Unavailable'); + div class => 'warning'; + h1 'Function Unavailable'; + p "The function you tried to access was set unavailable. This action is either not written yet, or was disabled by the server administrator. As soon as this function becomes available, it will be announced on the front page."; + end; + $self->htmlFooter; +} + +################################################################################ +# Error 400 +################################################################################ +sub handle400 { + my $self = shift; + + $self->resStatus(400); + $self->htmlHeader(title => '400 - Bad Request'); + div class => 'warning'; + h1 '400 - Bad Request'; + p "The server was unable to understand the request and process it."; + end; + $self->htmlFooter; +} + +################################################################################ +# Error 404 +# I either screwed up with a link, or the page you are looking for does not +# exist. A little more content would be nice, and perhaps a funny picture. +################################################################################ +sub handle404 { + my $self = shift; + + $self->resStatus(404); + $self->htmlHeader(title => '404 - Not Found'); + div class => 'warning'; + h1 '404 - Not Found'; + p; + txt 'It seems the page you were looking for does not exist,'; + br; + txt 'you may want to try using the menu to find what you are looking for.'; + end; + end; + $self->htmlFooter; +} + +################################################################################ +# Error 405 +################################################################################ +sub handle405 { + my $self = shift; + + $self->resStatus(405); + $self->htmlHeader(title => '405 - Method Not Allowed'); + div class => 'warning'; + h1 '405 - Method Not Allowed'; + p "The submitted method is not allowed."; + end; + $self->htmlFooter; +} + +################################################################################ +# Error 413 +################################################################################ +sub handle413 { + my $self = shift; + + $self->resStatus(413); + $self->htmlHeader(title => '413 - Request Entity Too Large'); + div class => 'warning'; + h1 '413 - Request Entity Too Large'; + p "The requested entity contains too many bytes."; + end; + $self->htmlFooter; +} + +################################################################################ +# Error 500 +# Internal server error, most likely due to a database freezing/being busy +################################################################################ +sub handle500 { + my($self, $error) = @_; + + $self->resStatus(500); + $self->htmlHeader(title => '500 - Internal Server Error'); + div class => 'warning'; + h1 '500 - Internal Server Error'; + p 'Something went wrong on our side. The problem was logged and will be fixed shortly. Please try again later.'; + + if ($self->debug) { + div class => "code"; + pre $error; + end; + } + end; + $self->htmlFooter; +} + +1; diff --git a/lib/MasterWebInterface/Handler/Tools/AddNew.pm b/lib/MasterWebInterface/Handler/Tools/AddNew.pm new file mode 100755 index 0000000..dbeaa90 --- /dev/null +++ b/lib/MasterWebInterface/Handler/Tools/AddNew.pm @@ -0,0 +1,280 @@ +package MasterWebInterface::Handler::Tools::AddNew; +use strict; +use warnings; +use Encode; +use Socket; +use IP::Country::Fast; +use TUWF ':html'; + +TUWF::register( + qr{new} => \&addnewserver, + qr{new/([\.\w]+):(\d+)/(\d+)} => \&valnewserver, +); + +################################################################################ +# Helper page to add server addresses to the masterserver manually +# Uses the valnewserver page/function to validate an active server. +################################################################################ +sub addnewserver { + my $self = shift; + $self->htmlHeader(title => "Add a new server"); + + div class => "mainbox detail"; + div class => "header"; + h1 "Manually add a server"; + p class => "alttitle", "333networks allows you to add supported servers manually. On this page is explained how to add your server to our masterserver."; + end; + + p "You can add your server to our site in two ways:"; + ol; + li; + txt "Follow the instructions on the "; + a href => "/masterserver", "MasterServer"; + txt " page. This also allows other players to see your server online."; + end; + li "Follow the instructions below. This allows you to share links to your server page."; + end; + + p; + txt "To link to your serverstatus, fill in your gameserver's IP and your gameserver's queryport, usually the game port +1. If your server does not show up, check for typos and verify that your firewall is not blocking your server. Your ip is "; + span class => "ext", $ENV{'REMOTE_ADDR'}; + txt "."; + end; + + table class => "shareopts new"; + Tr; + td "IP address:"; + td; input type => "text", class => "text", id => "ip", value => $ENV{'REMOTE_ADDR'}; end; + end; + Tr; + td "Query port (game port + 1):"; + td; input type => "text", class => "text", id => "port", value => "7778"; end; + end; + Tr; + td ""; + td; input type => "submit", class => "submit", value => "Search Server", onclick => "QueryLink()"; end; + end; + end; + + p id => "newlink", class => "ext"; + txt "Please enter the server's ip and port in the fields above."; + end; + + end; + + div id => "validate"; + end; + + $self->htmlFooter; +} + + +################################################################################ +# Query and validate a manually added server. +# Arguments passed on via javascript (unsafe) +# Random number prevents some browsers from caching the request and also +# prevents the /gamename/ip:port regex from catching this function/page. +################################################################################ +sub valnewserver { + my ($self, $s_addr, $s_port, $s_rand) = @_; + my ($ip,$port) = $self->valid_address($s_addr, $s_port); + + # return "invalid" if no valid ip/port + if (!$ip || !$port){die "invalid";return;} + + # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + # DANGEROUS CODE: DO NOT EDIT UNLESS YOU KNOW WHAT YOU ARE DOING + # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + # + # Query the game server here on the spot. This may cause timeouts/errors with + # slow or unresponsive servers. Will generate an error in the browser if so. + # + # TODO: consider safer code for this part. + # + # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + + # prepare target + my $qaddr = sockaddr_in($port, inet_aton($ip) ); + my ($data, $buf) = ("",""); + + eval { + # return error stats on time-out + local $SIG{ALRM} = sub {die "timeout"}; + + alarm 2; + socket(SERVER, PF_INET, SOCK_DGRAM, getprotobyname("udp")) or die "timeout"; + connect(SERVER, $qaddr); + send(SERVER, "\\status\\", 0, $qaddr); + + #receive server info + while($data !~ /\\final\\/) { + recv(SERVER, $data, 0xFFFF, 0); + $buf .= $data; + } + shutdown(SERVER, 2); + alarm 0; + }; + + # turn buffer into hashref + my @a = split /\\/, encode('UTF-8', $buf || ""); + shift @a; + my %h = (@a, (scalar @a % 2 == 1) ? "dummy" : () ); + %h = map { lc $_ => $h{$_} } keys %h; + my $r = \%h; + + # any text received? + # check for some random, supposedly existing tags like gamename, gamever + if ($r->{gamename} || $r->{gamever}) { + div class => "mainbox detail"; + div class => "header"; + h1 $r->{hostname} || "Unnamed Server"; + end; + + div class => "container"; + div class => "thumbnail"; + + # find the correct thumbnail, otherwise standard 333 esrb pic + my $mapfig = "$self->{map_url}/default/333esrb.jpg"; + + # get prefix and mapname + my $mapname = lc $r->{mapname}; + my ($pre,$post) = $mapname =~ /^(DM|CTF\-BT|BT|CTF|DOM|AS|JB|TO|SCR|MH)-(.*)/i; + my $prefix = ($pre ? uc $pre : "other"); + + # if map figure exists, use it + if (-e "$self->{map_dir}/$r->{gamename}/$prefix/$mapname.jpg") { + $mapfig = "$self->{map_url}/$r->{gamename}/$prefix/$mapname.jpg"; + } + + # if not, game default image + elsif (-e "$self->{map_dir}/default/$r->{gamename}.jpg") { + $mapfig = "$self->{map_url}/default/$r->{gamename}.jpg"; + } + + img src => $mapfig, + alt => $mapfig, + title => ($r->{mapname} || "Unknown"); + span ($r->{maptitle} || "Unknown"); + end; + + table class => "mapinfo"; + if ($r->{maxplayers}) { + Tr; + td class => "wc1", "Players:"; + td; + txt $r->{numplayers} || 0; + txt "/"; + txt $r->{maxplayers} || 0; + end; + end; + } + if ($r->{botskill} && $r->{minplayers}) { + Tr; + td "Bots:"; + td; + txt $r->{minplayers} || 0; + txt " "; + txt ($r->{botskill} || "Standard"); + txt " bot"; txt ($r->{minplayers} == 1 ? "" : "s"); + end; + end; + } + end; + end; # container + + # + # Specific server entry information + # + table class => "serverinfo"; + Tr; + th class => "wc1", "Server Info"; + th ""; + end; + Tr; + td "Address:"; + td title => $r->{port}, (($r->{ip} || $ip). ":". ($r->{hostport} || $port)); + end; + if ($r->{adminname}) { + Tr; + td "Admin:"; + td $r->{adminname}; + end; + } + Tr; + td class => "wc1", "Contact:"; + td; + if ($r->{adminemail}) {txt $r->{adminemail}} + end; + end; + Tr; + td class => "wc1", "Location:"; + my $reg = IP::Country::Fast->new(); + my ($flag, $country) = $self->countryflag($reg->inet_atocc($ip) || ""); + td; + img class => "flag", src => "/flag/$flag.svg"; + txt " ". $country; + end; + end; + end; + + # + # Specific game and version information + # + table class => "gameinfo"; + Tr; + th class => "wc1", "Game Info"; + th ""; + end; + Tr; + td "Game:"; + td $self->dbGetGameDesc($r->{gamename}) || "unknown game"; + end; + if ($r->{gametype}) { + Tr; + td "Type:"; + td $r->{gametype}; + end; + } + if ($r->{gamestyle}) { + Tr; + td "Style:"; + td $r->{gamestyle}; + end; + } + if ($r->{gamever}) { + Tr; + td "Version:"; + td $r->{gamever}; + end; + } + end; + + # + # Mutator list + # + table class => "mutators"; + Tr; + th "Mutators"; + end; + Tr; + td; + if (defined $r->{mutators}) { + txt $r->{mutators};} + else {i "This server does not have any mutators listed.";} + end; + end; + end; + + # add this server to the list of pending IPs if it does not exist in the db already + p class => "ext", ( $self->dbAddServer(ip => $ip, port => $port) ? "The server was added to the database." : "The server already exists in the database." ); + end; # mainbox detail + + } + else { + # else return "invalid" to make AJAX understand that the query failed. + txt "invalid"; + } +} + +1; + diff --git a/lib/MasterWebInterface/Handler/Tools/Version.pm b/lib/MasterWebInterface/Handler/Tools/Version.pm new file mode 100755 index 0000000..9ffe41a --- /dev/null +++ b/lib/MasterWebInterface/Handler/Tools/Version.pm @@ -0,0 +1,50 @@ +package MasterWebInterface::Handler::Tools::Version; +use strict; +use warnings; +use TUWF ':html'; + +TUWF::register( + qr{version}, \&version, +); + +sub version { + my $self = shift; + $self->htmlHeader(title => 'Version information', noindex => 1); + + div class => "mainbox contact"; + div class => "header"; + h1 "Version Information"; + p class => "alttitle", ""; + end; + + # version and author information + # + # 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 (e)mail with your compliments. + # + + p "This MasterServer Interface has the following version information:"; + table; + Tr; td "build_type"; td "333networks Masterserver Development Interface ";end; + Tr; td "build_version"; td "3.0.0";end; + Tr; td "build_date"; td "2017-09-25";end; + Tr; td "build_author"; td "Darkelarious, darkelarious\@333networks.com";end; + end; + + p "This MasterServer Interface is compatible since the following MasterServer type(s):"; + table; + Tr; td "build_type"; td "333networks Masterserver-Perl";end; + Tr; td "build_version"; td "2.4.1";end; + Tr; td "build_date"; td "2017-09-25";end; + Tr; td "build_author"; td "Darkelarious, darkelarious\@333networks.com";end; + end; + + end; +} + +1; + diff --git a/lib/MasterWebInterface/Util/BrowseHTML.pm b/lib/MasterWebInterface/Util/BrowseHTML.pm new file mode 100755 index 0000000..32d773e --- /dev/null +++ b/lib/MasterWebInterface/Util/BrowseHTML.pm @@ -0,0 +1,126 @@ +package MasterWebInterface::Util::BrowseHTML; +use strict; +use warnings; +use TUWF ':html', 'xml_escape'; +use Exporter 'import'; +use POSIX 'ceil'; +our @EXPORT = qw| htmlBrowse htmlBrowseNavigate |; + +# generates a browse box, arguments: +# items => arrayref with the list items +# options => hashref containing at least the keys s (sort key), o (order) and p (page) +# nextpage => whether there's a next page or not +# sorturl => base URL to append the sort options to (if there are any sortable columns) +# pageurl => base URL to append the page option to +# class => classname of the mainbox +# header => +# can be either an arrayref or subroutine reference, +# in the case of a subroutine, it will be called when the header should be written, +# in the case of an arrayref, the array should contain the header items. Each item +# can again be either an arrayref or subroutine ref. The arrayref would consist of +# two elements: the name of the header, and the name of the sorting column if it can +# be sorted +# row => subroutine ref, which is called for each item in $list, arguments will be +# $self, $item_number (starting from 0), $item_value +# footer => subroutine ref, called after all rows have been processed +# Mostly written by Yorhel --> https://g.blicky.net/vndb.git/tree/COPYING +sub htmlBrowse { + my($self, %opt) = @_; + + $opt{sorturl} .= $opt{sorturl} =~ /\?/ ? ';' : '?' if $opt{sorturl}; + + # top navigation + $self->htmlBrowseNavigate($opt{pageurl}, $opt{options}{p}, $opt{nextpage}, 't') if $opt{pageurl}; + + div class => 'mainbox browse'.($opt{class} ? ' '.$opt{class} : ''); + table class => 'stripe'; + + # header + thead; + Tr; + if(ref $opt{header} eq 'CODE') { + $opt{header}->($self); + } else { + for(0..$#{$opt{header}}) { + if(ref $opt{header}[$_] eq 'CODE') { + $opt{header}[$_]->($self, $_+1); + } elsif ($opt{simple}) { + td class => $opt{header}[$_][3]||'tc'.($_+1), $opt{header}[$_][2] ? (colspan => $opt{header}[$_][2]) : (); + if($opt{header}[$_][1]) { + lit qq|<a href="$opt{sorturl}o=d;s=$opt{header}[$_][1]">$opt{header}[$_][0]</a>|;} + else { + txt $opt{header}[$_][0];} + end; + } else { + td class => $opt{header}[$_][3]||'tc'.($_+1), $opt{header}[$_][2] ? (colspan => $opt{header}[$_][2]) : (); + lit $opt{header}[$_][0]; + if($opt{header}[$_][1]) { + lit ' '; + $opt{options}{s} eq $opt{header}[$_][1] && $opt{options}{o} eq 'a' ? lit "\x{25B4}" : a href => "$opt{sorturl}o=a;s=$opt{header}[$_][1]", "\x{25B4}"; + $opt{options}{s} eq $opt{header}[$_][1] && $opt{options}{o} eq 'd' ? lit "\x{25BE}" : a href => "$opt{sorturl}o=d;s=$opt{header}[$_][1]", "\x{25BE}"; + } + end; + } + } + } + end; + end 'thead'; + + # footer + if($opt{footer}) { + tfoot; + $opt{footer}->($self); + end; + } + + # rows + $opt{row}->($self, $_+1, $opt{items}[$_]) + for 0..$#{$opt{items}}; + + end 'table'; + end 'div'; + + # bottom navigation + $self->htmlBrowseNavigate($opt{pageurl}, $opt{options}{p}, $opt{nextpage}, 'b') if $opt{pageurl}; +} + + +# creates next/previous buttons (tabs), if needed +# Arguments: page url, current page (1..n), nextpage (0/1 or [$total, $perpage]), alignment (t/b), noappend (0/1) +# Mostly written by Yorhel --> https://g.blicky.net/vndb.git/tree/COPYING +sub htmlBrowseNavigate { + my($self, $url, $p, $np, $al, $na) = @_; + my($cnt, $pp) = ref($np) ? @$np : ($p+$np, 1); + return if $p == 1 && $cnt <= $pp; + + $url .= $url =~ /\?/ ? ';p=' : '?p=' unless $na; + + my $tab = sub { + my($left, $page, $label) = @_; + li $left ? (class => 'left') : (); + a href => $url.$page; lit $label; end; + end; + }; + my $ell = sub { + use utf8; + li class => 'ellipsis'.(shift() ? ' left' : ''); + b '⋯'; + end; + }; + my $nc = 5; # max. number of buttons on each side + + ul class => 'maintabs browsetabs ' . ($al eq 't' ? 'notfirst' : 'bottom'); + $p > 2 and ref $np and $tab->(1, 1, '« first'); + $p > $nc+1 and ref $np and $ell->(1); + $p > $_ and ref $np and $tab->(1, $p-$_, $p-$_) for (reverse 2..($nc>$p-2?$p-2:$nc-1)); + $p > 1 and $tab->(1, $p-1, '‹ previous'); + + my $l = ceil($cnt/$pp)-$p+1; + $l > 2 and $tab->(0, $l+$p-1, ('last').' »'); + $l > $nc+1 and $ell->(0); + $l > $_ and $tab->(0, $p+$_, $p+$_) for (reverse 2..($nc>$l-2?$l-2:$nc-1)); + $l > 1 and $tab->(0, $p+1, ('next').' ›'); + end 'ul'; +} + +1; diff --git a/lib/MasterWebInterface/Util/CommonHTML.pm b/lib/MasterWebInterface/Util/CommonHTML.pm new file mode 100755 index 0000000..5390740 --- /dev/null +++ b/lib/MasterWebInterface/Util/CommonHTML.pm @@ -0,0 +1,26 @@ +package MasterWebInterface::Util::CommonHTML; +use strict; +use warnings; +use TUWF ':html'; +use Exporter 'import'; +our @EXPORT = qw| htmlSearchBox |; + +################################################################################ +# Search box with first letters +# for games, servers and possibly later on players +################################################################################ +sub htmlSearchBox { + my($self, $sel, $v) = @_; + + fieldset class => 'search'; + p id => 'searchtabs'; + a href => '/g/all', $sel eq 'g' ? (class => 'sel') : (), 'Games'; + a href => '/s/all', $sel eq 's' ? (class => 'sel') : (), 'Servers'; + #a href => '/p/all', $sel eq 'p' ? (class => 'sel') : (), 'Players'; + end; + input type => 'text', name => 'q', id => 'q', class => 'text', value => $v; + input type => 'submit', class => 'submit', value => 'search'; + end 'fieldset'; +} + +1; diff --git a/lib/MasterWebInterface/Util/Figures.pm b/lib/MasterWebInterface/Util/Figures.pm new file mode 100755 index 0000000..a3f3e5f --- /dev/null +++ b/lib/MasterWebInterface/Util/Figures.pm @@ -0,0 +1,38 @@ +package MasterWebInterface::Util::Figures; +use strict; +use warnings; +use TUWF ':html'; +use Exporter 'import'; +use Image::Size; +our @EXPORT = qw| figure figurelink |; + +################################################################################ +# Load image in Yorhel's IV. +################################################################################ +sub figure { + my ($self, $d, $f, $s) = @_; + my $extra_css = (defined($s)) ? "style=\"$s\"" : ""; + my ($w, $h) = imgsize("$self->{img_path}/$d/$f"); + # make a link and show a thumbnail if exists, else photo itself + if (-e "$self->{img_path}/t/$f") { + lit "<a rel=\"iv:$w"."x"."$h\" href=\"/img/$d/$f\"><img $extra_css src=\"/img/t/$f\" alt=\"$f\"/></a> " + } + else{ + lit "<a rel=\"iv:$w"."x"."$h\" href=\"/img/$d/$f\"><img $extra_css src=\"/img/$d/$f\" alt=\"$f\"/></a> "} +} + +################################################################################ +# Have a picture $f link to destination $dest -- wrapper function +################################################################################ +sub figurelink { + my ($self, $d, $f, $dest) = @_; + # make a link and show a thumbnail if exists, else photo itself + if (-e "$self->{img_path}/t/$f") { + lit "<a href=\"$dest\"><img src=\"/img/t/$f\" alt=\"$f\"/></a> " + } + else{ + lit "<a href=\"$dest\"><img src=\"/img/$d/$f\" alt=\"$f\"/></a> "} +} + + +1; diff --git a/lib/MasterWebInterface/Util/Layout.pm b/lib/MasterWebInterface/Util/Layout.pm new file mode 100755 index 0000000..4f62d5e --- /dev/null +++ b/lib/MasterWebInterface/Util/Layout.pm @@ -0,0 +1,60 @@ +package MasterWebInterface::Util::Layout; +use strict; +use warnings; +use TUWF ':html'; +use Exporter 'import'; +our @EXPORT = qw| htmlHeader htmlFooter |; + +################################################################################ +# htmlHeader +# options: title, noindex +################################################################################ +sub htmlHeader { + my($self, %o) = @_; + + html lang => "en"; + head; + title "$o{title} :: $self->{site_title}"; + Link rel => 'shortcut icon', href => "$self->{url}/favicon.ico", type => 'image/x-icon'; + Link rel => 'stylesheet', href => "$self->{url}/style/$self->{style}/style.css", type => "text/css", media => "all"; + meta name => "google-site-verification", content => "tkhIW87EwqNKSGEumMbK-O3vqhwaANWbNxkdLBDGZvI";end; + meta name => 'robots', content => 'noindex,nofollow,nosnippet,noodp,noarchive,noimageindex';end; #FIXME set proper robots params + script type => 'text/javascript', src => "$self->{url}/interface-scripts.js", ''; + end; # head + + body; + div class => 'nav'; + ul; + li; a href => "/", "home"; end; + li; a href => "/g/all", "games"; end; + li; a href => "/s/all", "servers"; end; + end; + end; + + div id => "body"; + # start the page content with a header logo box + div class => "titlebox"; + end; +} + +################################################################################ +# htmlFooter +# options: last edited (not shown) +# General html layout header (bottom) +################################################################################ +sub htmlFooter { + my $self = shift; + br style => "clear:both"; + + div id => 'footer'; + txt "$self->{site_title} | Powered by "; + a href => "http://333networks.com", "333networks"; + txt " & "; + a href => "http://dev.yorhel.nl/tuwf", "TUWF"; + end; + end 'div'; # body + end 'body'; + end 'html'; +} + +1; diff --git a/lib/MasterWebInterface/Util/Misc.pm b/lib/MasterWebInterface/Util/Misc.pm new file mode 100755 index 0000000..0be7aae --- /dev/null +++ b/lib/MasterWebInterface/Util/Misc.pm @@ -0,0 +1,72 @@ +package MasterWebInterface::Util::Misc; +use strict; +use warnings; +use TUWF ':html'; +use POSIX 'strftime'; +use Exporter 'import'; +use Encode 'encode_utf8'; +use Geography::Countries; +use Unicode::Normalize 'NFKD'; +use Socket 'inet_pton', 'inet_ntop', 'AF_INET', 'AF_INET6'; +our @EXPORT = qw| date_new timeformat countryflag valid_address |; + +################################################################################ +# time formatting for when a server was added +################################################################################ +sub date_new { + my ($s, $d) = @_; + #return (strftime "%a %H:%M:%S", gmtime $d); + return (strftime "%a %H:%M", gmtime $d); # no seconds +} + +################################################################################ +# time formatting for when a server was added / last updated +################################################################################ +sub timeformat { + my ($self, $time) = @_; + + # parse seconds with gmtime + my @t = gmtime($time); + my $r = ""; + + # parse into d HH:mm:SS format + if ($t[7]){$r .= $t[7]."d "} + if ($t[2]){$r .= ($t[2] > 9) ? $t[2].":" : "0".$t[2].":" } + if ($t[1]){$r .= ($t[1] > 9) ? $t[1].":" : "0".$t[1].":" } else {$r .= "00:";} + if ($t[0]){$r .= ($t[0] > 9) ? $t[0] : "0".$t[0] } else {$r .= "00";} + + return $r; +} + +################################################################################ +# returns flag, country name +################################################################################ +sub countryflag { + my ($self, $c) = @_; + my $flag = ($c ? lc $c : 'earth'); + my $coun = $c ? ( $c eq 'EU' ? 'Europe' : country $c ) : 'Earth' ; + return $flag, $coun; +} + +################################################################################ +# Verify whether a given domain name or IP address and port are valid. +# returns the valid ip-address + port, or 0 when not. +################################################################################ +sub valid_address { + my ($self, $a, $p) = @_; + + # check if ip and port are in valid range + 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 local addresses + if ($a =~ m/192.168.(\d).(\d)/ || $a =~ m/127.0.(\d).(\d)/ || $a =~ m/10.0.(\d).(\d)/) { $val_addr = 0; } + + # return valid params + return ( + $val_addr ? $a : 0, + $val_port ? $p : 0 + ); +} + +1; diff --git a/log/logs.txt b/log/logs.txt new file mode 100755 index 0000000..e27ccc5 --- /dev/null +++ b/log/logs.txt @@ -0,0 +1 @@ +Placeholder for logs diff --git a/s/favicon.ico b/s/favicon.ico Binary files differnew file mode 100755 index 0000000..1182ce4 --- /dev/null +++ b/s/favicon.ico diff --git a/s/img/other/erroristforum.jpg b/s/img/other/erroristforum.jpg Binary files differnew file mode 100644 index 0000000..2ba8fcc --- /dev/null +++ b/s/img/other/erroristforum.jpg diff --git a/s/img/t/ubrowser2.jpg b/s/img/t/ubrowser2.jpg Binary files differnew file mode 100644 index 0000000..efe7de4 --- /dev/null +++ b/s/img/t/ubrowser2.jpg diff --git a/s/interface-scripts.js b/s/interface-scripts.js new file mode 100755 index 0000000..a8184e1 --- /dev/null +++ b/s/interface-scripts.js @@ -0,0 +1,161 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * *\ + * 333networks site * + * Written by Darkelarious * + * Please see http://git.333networks.com/ for * + * questions and copyright. * +\* * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +function DOMLoad(y){var d=0;var f=function(){if(d++)return;y()};if(document.addEventListener)document.addEventListener("DOMCont"+"entLoaded",f,false);document.write("<script id=_ie defer src="+"javascript:void(0)><\/script>");document.getElementById('_ie').onreadystatechange=function(){if(this.readyState=="complete")f()};if(/WebKit/i.test(navigator.userAgent))var t=setInterval(function(){if(/loaded|complete/.test(document.readyState)){clearInterval(t);f()}},10);window.onload=f;} + +// spam protection on all forms +setTimeout(function() { + for(i=0; i<document.forms.length; i++) + document.forms[i].action = document.forms[i].action.replace(/\/nospam\?/,''); +}, 500); + +function x(y){return document.getElementById(y)} + +// show/hide scripts for big blocks of code +function show(x){ + var xdiv = document.getElementById(x); + xdiv.style.display = (xdiv.style.display == 'block') ? 'none' : 'block'; +} + +// IV script by YorHel +var ivL=[]; +function ivInit() { + var l=document.getElementsByTagName('a'); + for(i=0;i<l.length;i++) + if(l[i].rel && l[i].rel.indexOf('iv:')==0) { + l[i].onclick=ivView; + ivL[ivL.length] = l[i]; + } + + var d = document.createElement('div'); + d.id = 'ivview'; + d.innerHTML = '<b id="ivimg"></b><br />' + +'<a href="#" id="ivfull"> </a>' + +'<a href="#" onclick="return ivClose()" id="ivclose">close</a>' + +'<a href="#" onclick="return ivView(this)" id="ivprev">« previous</a>' + +'<a href="#" onclick="return ivView(this)" id="ivnext">next »</a>'; + document.body.appendChild(d); + d = document.createElement('b'); + d.id = 'ivimgload'; + d.innerHTML = 'Loading...'; + document.body.appendChild(d); +} +function ivView(what) { + what = what && what.rel ? what : this; + var u=what.href; + var r=what.rel; + d = x('ivview'); + + // fix prev/next links (if any) + for(var i=0;i<ivL.length;i++) + if(ivL[i].href == u) { + x('ivnext').style.visibility = ivL[i+1] ? 'visible' : 'hidden'; + x('ivnext').href = ivL[i+1] ? ivL[i+1].href : '#'; + x('ivnext').rel = ivL[i+1] ? ivL[i+1].rel : ''; + x('ivprev').style.visibility = ivL[i-1] ? 'visible' : 'hidden'; + x('ivprev').href = ivL[i-1] ? ivL[i-1].href : '#'; + x('ivprev').rel = ivL[i-1] ? ivL[i-1].rel : ''; + } + + // calculate dimensions + r = r.substring(3); + var w = Math.floor(r.split('x')[0]); + var h = Math.floor(r.split('x')[1]); + var ww = typeof(window.innerWidth) == 'number' ? window.innerWidth : document.documentElement.clientWidth; + var wh = typeof(window.innerHeight) == 'number' ? window.innerHeight : document.documentElement.clientHeight; + var st = typeof(window.pageYOffset) == 'number' ? window.pageYOffset : document.body && document.body.scrollTop ? document.body.scrollTop : document.documentElement.scrollTop; + if(w+100 > ww || h+70 > wh) { + x('ivfull').href = u; + x('ivfull').innerHTML = w+'x'+h; + x('ivfull').style.visibility = 'visible'; + if(w/h > ww/wh) { // width++ + h *= (ww-100)/w; + w = ww-100; + } else { // height++ + w *= (wh-70)/h; + h = wh-70; + } + } else + x('ivfull').style.visibility = 'hidden'; + var dw = w; + var dh = h+20; + dw = dw < 200 ? 200 : dw; + + // update document + d.style.display = 'block'; + x('ivimg').innerHTML = '<img src="'+u+'" onclick="ivClose()" onload="document.getElementById(\'ivimgload\').style.top=\'-400px\'" style="width: '+w+'px; height: '+h+'px" />'; + d.style.width = dw+'px'; + d.style.height = dh+'px'; + d.style.left = ((ww - dw) / 2 - 10)+'px'; + d.style.top = ((wh - dh) / 2 + st - 20)+'px'; + x('ivimgload').style.left = ((ww - 100) / 2 - 10)+'px'; + x('ivimgload').style.top = ((wh - 20) / 2 + st)+'px'; + return false; +} +function ivClose() { + x('ivview').style.display = 'none'; + x('ivview').style.top = '-5000px'; + x('ivimg').innerHTML = ''; + return false; +} + +// create new server entry page +var t; +function QueryLink() +{ + clearTimeout(t); + t = setTimeout("QueryServer()", 2000); + + var newlink = x("newlink"); + newlink.innerHTML = "Typing..."; + newlink.style.color = "#aaa"; +} + + +// create new server entry page +function QueryServer() { + var ip = x("ip"); + var port = x("port"); + + var newlink = x("newlink"); + var validate = x("validate"); + + var ValAddress = "/new/"+ip.value+":"+port.value+"/"+(Math.floor(Math.random()*999)+1); + + newlink.innerHTML = "Checking..."; + newlink.style.color = "#aa5"; + + var check = (window.ActiveXObject) ? new ActiveXObject('Microsoft.XMLHTTP') : new XMLHttpRequest(); + + check.onreadystatechange = function (){ + if (check.readyState==4 && check.status==200){ + var rx = check.responseText; + if (rx == "invalid"){ + newlink.innerHTML = "This server appears to be offline, unreachable or is not supported. Please check your values and try again."; + newlink.style.color = "#a55"; + validate.style.display = "none"; + }else{ + newlink.innerHTML = "<a href=\"/ut/"+ip.value+":"+port.value+"\">Success! Click here to view your server in our database!</a>"; + validate.innerHTML = rx; + validate.style.display = "block"; + } + }else if (check.status==500){ + newlink.innerHTML = "An error occurred while processing the response. Please check your values and try again."; + newlink.style.color = "#a55"; + validate.style.display = "none"; + } + } + + check.open("GET", ValAddress, true); + check.send(null); +} + + +/* O N L O A D */ +DOMLoad(function(){ + ivInit(); +}); diff --git a/s/map/default/333esrb.jpg b/s/map/default/333esrb.jpg Binary files differnew file mode 100644 index 0000000..8b223b1 --- /dev/null +++ b/s/map/default/333esrb.jpg diff --git a/s/map/default/333networks.jpg b/s/map/default/333networks.jpg Binary files differnew file mode 100644 index 0000000..8b223b1 --- /dev/null +++ b/s/map/default/333networks.jpg diff --git a/s/map/default/ut.jpg b/s/map/default/ut.jpg Binary files differnew file mode 100755 index 0000000..7c5beef --- /dev/null +++ b/s/map/default/ut.jpg diff --git a/s/masterwebinterface.pl b/s/masterwebinterface.pl new file mode 120000 index 0000000..7f1fb2c --- /dev/null +++ b/s/masterwebinterface.pl @@ -0,0 +1 @@ +../util/masterwebinterface.pl
\ No newline at end of file diff --git a/s/robots.txt b/s/robots.txt new file mode 100755 index 0000000..6ffbc30 --- /dev/null +++ b/s/robots.txt @@ -0,0 +1,3 @@ +User-agent: * +Disallow: / + diff --git a/s/style/basic/333networks.png b/s/style/basic/333networks.png Binary files differnew file mode 100755 index 0000000..9803dc1 --- /dev/null +++ b/s/style/basic/333networks.png diff --git a/s/style/basic/conf b/s/style/basic/conf new file mode 100644 index 0000000..6223459 --- /dev/null +++ b/s/style/basic/conf @@ -0,0 +1,28 @@ +// Basic greyscale theme with 333networks logo +// see util/skingen/colortypes.txt for descriptions of colors. + +name basic +author Darkelarious + +bodybg #fff +bglogo 333networks.png + +boxbg1 #ccc +boxbg2 #999 +boxbg3 #aaa + +shadow #999 + +textcol1 #000 +textcol2 #222 + +headercol #000 +themecol1 #000 +themecol2 #555 +themecol3 #222 + +link #33f +linkhover #f33 + + + diff --git a/s/style/basic/style.css b/s/style/basic/style.css new file mode 100644 index 0000000..6056252 --- /dev/null +++ b/s/style/basic/style.css @@ -0,0 +1,887 @@ +/* * * * * * * * * * * * * * * * * * *\ +* Master Server Web Interface * +* A website by Darkelarious * +* See COPYING for additional details * +* on re-use. * +\* * * * * * * * * * * * * * * * * * */ + +/* Theme basic written by Darkelarious */ + +/*** Global ***/ +* { + margin: 0; + padding: 0; + border: 0; + outline:0; + } +body { + text-align: center; + height: 100%; + color: #000; + font-family: 'Verdana'; + font-size: 10pt; + background: #fff; + } + +p { + margin-bottom:12pt; + } +a, a:visited { + text-decoration: none; + color: #33f; + } +a:hover { + color: #f33; + } + +hr { + height: 0px; + border: solid #000 0px; + border-top-width: 1px; + margin: 10pt 0 10pt 0; + } + +/*** Container ***/ +#body { + text-align: left; + width: 900px; + margin: 0 auto; + min-height: 100%; + padding-top:100px; + background: url(/style/basic/333networks.png) no-repeat center 35px fixed; + } +#footer{ + padding: 90px 0 40px 0; + text-align:center; + font-size : x-small; + clear:both + } +#footer a{ + text-decoration: underline; + } + + +/*** navigation box ***/ +div.nav { + position: fixed; + top:0px; + display:block; + width:100%; + background: #999; + padding:0; + border-bottom: 1px solid #000; + } +div.nav ul { + list-style:none; + text-align:center; + } +div.nav ul li { + width:90px; + display: inline-block; + padding:5px 0; + font-size:14px; + } +div.nav ul li ul { + margin:0; + position:absolute; + visibility:hidden; + } +div.nav ul li:hover ul { + list-style:none; + visibility:visible; + margin-top:5px; + background: #999; + border:1px solid #000; + border-top:0px; + } +div.nav ul li:hover ul li { + display:block; + } +div.nav ul li:hover ul li a, +div.nav ul li:hover ul li span{ + padding: 2px 0 2px 5px; + } +div.nav ul li:hover ul li a:hover, +div.nav ul li:hover ul li span:hover{ + } + +/*** common types (box, form, lists, headers) ***/ +div.mainbox { + border: 1px solid #000; + margin: 21px 0 -10px 0; + padding: 5px; + background: #ccc; + +} + +.mainbox h1, +.mainbox h2, +.mainbox h3, +.mainbox h4 { + font-family: Tahoma; + font-weight: bold; + font-size: 19px; + color: #000; +} +.mainbox h1 { + margin: -5px 0 15px 0; + text-decoration: none; + } +.mainbox h2 { + margin: 5px 0 0 5px; + font-size: 12pt; + } +.mainbox h2.alttitle { + margin: -17px 0 15px 15px; + font-weight: normal; + } +.mainbox h3 { + margin: 5px 0 0 5px; + font-size: 11pt; + } +.mainbox h4 { + margin: 5px 0 0 5px; + font-size: 10pt; + text-decoration: underline; + } + +.mainbox p { + margin:5px 8px 10pt 8px; + text-align:left; + } +.mainbox ul, +.mainbox ol { + margin: 3px 20px 10pt 40px; + } +.mainbox div.header { + margin: -5px 2px 10px -5px; + padding:10px 0 0 10px; + display:block; + width:100%; + color: #000; + border-bottom:1px solid #000; + background: #999; + } +.mainbox div.header h2 { + text-decoration: none; + margin-bottom:5px; + } +.mainbox div.header p.alttitle { + margin-bottom:0px; + font-size: 8pt; + text-align:left; + padding:0 5px 5px 25px; + } +.mainbox div.header p.alttitle span.acc { + color: #f33; +} + +/* form markup */ +input.text, +input.submit, +select, +textarea { + background: #aaa; + color: #222; + border: 1px solid #000; + margin: 1px; + padding:1px; +} +form, fieldset { + border: 0; + display: block; + } +legend { + display: none; + } +optgroup option { + padding-left: 10px; + font-style: normal; + } +input.submit { + background: #999; + border: 1px solid #000; + padding: 0px; + } +input.text, select { + width: 200px; + } +fieldset.submit { + width: 100%; + text-align: center; + margin: 5px; + } +fieldset.submit input { + width: 150px; + } +fieldset.submit h2 { + font-size: 11px!important; + } +fieldset.submit textarea { + margin: 0 20px 5px 20px; + } +td.label, td.label label { + width: 90px; + } +td.label label { + display: block; + } +td.field label { + margin: 0 5px 0 5px; + } +table.formtable { + margin: 0 20px 20px 20px; + } +table.formtable td { + padding: 0; + } +table.formtable tr.newfield td { + padding-top: 5px; + } +table.formtable tr.newpart td { + padding-top: 20px; + font-weight: bold; + } + +/*** browsing table/form ***/ +ul.maintabs { + display: inline; + margin: 0; + } +ul.maintabs.notfirst { + display: block; + height: 20px + } +ul.maintabs li { + display: inline; + list-style-type: none + } +ul.maintabs li a, +ul.maintabs li b { + float: right; + display: block; + height: 14px; + padding: 1px 7px 5px 7px; + margin: 0 0 0 10px; +} +ul.maintabs li a { + margin-left: 5px; + border: 1px solid #000; + border-bottom: none; + background: #ccc; +} +ul.maintabs.notfirst li a, +ul.maintabs.notfirst li b { + margin-top: 20px + } +ul.maintabs.bottom li a, +ul.maintabs.bottom li b { + margin-top: 10px; + padding: 1px 7px 5px 7px + } +ul.maintabs.bottom li a { + border-bottom: 1px solid #000; + border-top: none + } +ul.maintabs li.left a, +ul.maintabs li.left b { + float: left; + margin-left: 0; + margin-right: 10px + } +ul.maintabs li b { + margin-left: -2px; + margin-right: -7px + } +ul.maintabs li.left b { + margin-left: -7px; + margin-right: -2px + } +ul.maintabs li.tabselected a, +ul.maintabs li a:hover { + padding-bottom: 6px + } +ul.maintabs.bottom li.tabselected a, +ul.maintabs.bottom li a:hover { + padding-top: 2px; + padding-bottom: 5px; + margin-top: 9px + } +ul.maintabs.browsetabs li a { + margin-left: 5px; + background: #ccc; + } +ul.maintabs.browsetabs li.left a { + margin-left: 0; + margin-right: 5px + } + +div.mainbox.browse table { + width:100%; + table-layout:fixed; + border-collapse:collapse + } +div.mainbox.browse table tr td { + white-space:nowrap; + overflow:hidden; + text-align:center; + padding:0 10px 0 5px; + font-size:10pt; + } +div.mainbox.browse table tr.odd { + background: #aaa; + } +div.mainbox.browse table thead tr td { + white-space:nowrap; + overflow:hidden; + text-align:center; + border-top:0; + border-bottom:1px solid #000; + color: #000; + padding:3px 10px 3px 5px; + background: #999; + font-weight:bold + } +div.mainbox.browse table tr td a{ + color: #000; + } +div.mainbox.browse table tr td a:hover{ + color: #f33; + } + +/* search box on top */ +p.browseopts a { + padding: 1px 3px; + border: 1px solid #33f; + margin: 0 2px; + white-space: nowrap; +} +p.browseopts { + text-align: center; + padding: 2px; + } +p.browseopts a.optselected, +p.browseopts a:hover { + border: 0; + padding: 2px 4px; + } +div.mainbox.browse { + padding: 0; + } +div.mainbox.browse table { + width: 100%; + } +table thead td { + font-weight: bold; + } +fieldset.search { + display: block; + width: 100%; + text-align: center; + margin: 0 0 10px 0; + } +fieldset.search .submit { + padding: 0 1px; + } +p#searchtabs { + height: 12px; + text-align:center; + } +p#searchtabs a { + padding: 2px 6px 2px 6px; + margin: 0 2px; + } +p#searchtabs a:hover, +p#searchtabs a.sel { + border: 1px solid #33f; + padding: 1px 5px 2px 5px; +} +p#searchtabs a:hover { + border: 1px solid #f33; + } +#q {width: 600px} +#bq {width: 300px} + +/*** Server Info details page ***/ +div.mainbox.detail { + padding:0 0 12px 0; + } +div.mainbox.detail div.header { + margin:0 0 12pt 0; + padding:3px 0 0 0; + display:block; + width:100%; + } +div.mainbox.detail div.header h1 { + padding: 15px 5px 0 15px; + } +div.mainbox.detail table { + margin-bottom: 20px; + table-layout:fixed; + border-collapse:collapse; + text-align:left; + } +div.mainbox.detail table tr th { + font-weight:bold; + padding: 2px 8px; + margin-bottom:12pt; + border-bottom: 1px solid #555 + } +div.mainbox.detail table tr th.wc1 { + width:170px + } +div.mainbox.detail table tr td { + vertical-align:top; + padding:6px 5px 4px 15px; + } +div.mainbox.detail table tr td img.flag { + margin:-4px 2px -4px 0; + height:20px; + width:30px + } +div.mainbox.detail table tr td input, +div.mainbox.detail table tr td textarea { + width:99%; + padding: 2px 0px 1px 4px; + border: 1px solid #000; + font-family:"Lucida Console"; + font-size: 8pt; + } +div.mainbox.detail table.serverinfo, +div.mainbox.detail table.gameinfo { + width:63%; + } +div.mainbox.detail table.teaminfo, +div.mainbox.detail table.limits, +div.mainbox.detail table.mutators, +div.mainbox.detail table.players, +div.mainbox.detail table.useropts, +div.mainbox.detail table.shareopts { + width:96%; + } +div.mainbox.detail div.container { + float:right; + margin-right:30px; + } +div.mainbox.detail div.container div.thumbnail { + border:1px solid #222; + background: #aaa; + display:block; + margin-bottom:8px + } +div.mainbox.detail div.container div.thumbnail img { + height:256px; + width:256px; + border:1px solid #555; + display:block; + margin:10px 10px 2px 10px + } +div.mainbox.detail div.container div.thumbnail span { + text-align: right; + font-weight: normal; + padding:0 15px 4px 5px; + display:block; + font-size:x-small + } +div.mainbox.detail div.container table.mapinfo { + width: 280px; + margin-bottom:0; + } +div.mainbox.detail div.container table.mapinfo tr td.wc1{ + width: 80px; + } +div.mainbox.detail table.players tr.odd { + background: #aaa; + } +div.mainbox.detail table.players tr td { + padding:1px 5px 0px 15px; + overflow:hidden; + white-space: nowrap; + } +div.mainbox.detail table.players tr th.frags {width:50px} +div.mainbox.detail table.players tr th.mesh {width:140px} +div.mainbox.detail table.players tr th.skin {width:140px} +div.mainbox.detail table.players tr th.ping {width:50px} + +div.mainbox.detail table.players tr td.frags, +div.mainbox.detail table.players tr th.frags, +div.mainbox.detail table.players tr td.ping, +div.mainbox.detail table.players tr th.ping {text-align:right;} + +div.mainbox.detail table.shareopts.new { + width:325px; + display:block; + margin:auto; + border-collapse: collapse; +} +div.mainbox.detail table.shareopts.new tr td { + white-space:nowrap; + padding:2px; +} +div.mainbox.detail table.shareopts.new tr td input { + width: 114px; + margin:0; +} +div.mainbox.detail table.shareopts.new tr td input.submit { + width:120px; +} +div.mainbox.detail p#newlink { + text-align:center; +} + + +/* Warning/Notice Box */ +div.warning, div.notice { + margin: 15px 10%; + padding: 15px; + color:#fff; + background-color: #411; + border: 1px solid #c00; + } +div.notice { + background-color: #141; + border: 1px solid #0c0; + } +div.warning ul, +div.notice ul { + margin-left: 0; + } +div.warning li, +div.notice li { + margin-left: 20px; + } +div.warning h2, +div.notice h2 { + font-size: 11px; + font-weight: bold; + margin: 0; + text-decoration: underline; + color: #fff + } +div.warning a{ + color: #f00 + } +div.notice a{ + color: #0f0 + } + +/* figures */ +.figure{ + text-align:center; + margin-bottom: 12pt + } +.figure img { + display:block; + margin: 5px auto 10px auto; + border:1px solid #000; + width:400px; + box-shadow: 10px 10px 5px #999; + } +.figure object{ + display:block; + margin: 5px auto 10px auto; + border:1px solid #000 + box-shadow: 10px 10px 5px #999; + } +.figure span{ + font-weight:bold + } +.figure .imgleft { + width:250px; + display:inline-block; + margin-right:10px + box-shadow: 10px 10px 5px #999; + } +.figure .imgright { + width:250px; + display:inline-block; + margin-left:10px + box-shadow: 10px 10px 5px #999; + } + +ol.steplist, +ul.steplist { + display: block; + width: 600px; + margin: 0 auto 12pt auto; + } +ol.steplist li, +ul.steplist li { + border-bottom:1px dotted #555; +} +ul.steplist.link { + width:200px; +} + +/*** Additional, minor styling details ***/ +.r {color:#f33} +.o {color:#c80} +.y {color:#ff3} +.g {color:#3f3} +.b {color:#33f} +.ext {color: #222; font-style: italic;} + +a.code { + font-family:"Lucida Console"; + font-size: 8pt; + text-align:left; + padding:5px; + border: 1px solid #000; + background: #999; + box-shadow: 10px 10px 5px #999; + } +pre.code, +div.code { + display:block; + margin:12pt auto; + width:90%; + font-family:"Lucida Console"; + font-size: 8pt; + text-align:left; + padding:5px; + border: 1px solid #000; + overflow:hidden; + background: #999; + box-shadow: 10px 10px 5px #999; + } +span.code { + font-family:"Lucida Console"; + font-size: 8pt; + text-align:left; + padding:2px; + background: #999; +} +.tiny { + font-size: 8pt; + text-align:left; + padding:0 5px 5px 25px; + color: #000; + } +.footnote{ + font-size: 8pt; + text-align:left; + padding:5px; + color: #000; + border-top: 1px solid #000; + } +a.addnew, +p.addnew { + float: right; + margin: 0 + } + +.debug *{ + border:1px; + } +.un { + text-align:left!important + } + + +/* browse table coloring */ +tr#tfooter td{ + padding:1px 1px 1px 1px; + border-top:1px solid #222; + } +tr#tfooter td a{ + color: #33f + } +tr#tfooter td a:hover { + color: #f33 + } + +/* Main page */ +div.frontleft { + float:left; width:444px; + } +div.frontright{ + float:right;width:444px; + } +div.frontcontainer { + margin-top: -18px; + } +div.frontcontainer img { + width: 442px; + margin:0px auto 10pt auto; + display:block; + border-bottom: 1px solid #000; + } +div.frontcontainer img.partner { + border: 0; +} +div.frontcontainer div.mainbox { + padding:0px; + } +div.frontcontainer div.mainbox h2 { + margin:0px -18px 5px 8px; + text-decoration:none; + } +div.frontcontainer div.mainbox div.header{ + margin: 0 0 3px 0; + padding-top:3px; + display:block; + width:432px + } +div.frontcontainer div.mainbox b { + margin:5px 8px 10pt 8px; + } + +/*** table alignments ***/ +div.mainbox.browse.newservers table tr td.tc1, +div.mainbox.browse.serverlist table tr td.tc1, +div.mainbox.browse.frontmasterlist table tr td.tc1 { + width:10px; +} + +div.mainbox.browse.newservers table tr td.tc1.flag, +div.mainbox.browse.serverlist table tr td.tc1.flag, +div.mainbox.browse.frontmasterlist table tr td.tc1.flag { + width:10px; + background-size: 20px 15px; + background-position: 50%; + background-repeat: no-repeat; + } + +div.mainbox.browse.newservers table tr td.tc2 {text-align:left} +div.mainbox.browse.newservers table tr td.tc3 {text-align:right; width:260px;} +div.mainbox.browse.newservers table tr td.tc4 {width:100px;} + +div.mainbox.browse.serverlist table tr td {text-align:left} + +div.mainbox.browse.serverlist table tr td.tc2 {} +div.mainbox.browse.serverlist table tr td.tc3 {width:60px;text-align:right} +div.mainbox.browse.serverlist table tr td.tc4 {width:90px;text-align:left} +div.mainbox.browse.serverlist table tr td.tc5 {width:70px;text-align:center} +div.mainbox.browse.serverlist table tr td.tc6 {width:140px;text-align:left} + + +div.mainbox.browse.gamelist table tr td.tc1 {text-align:left} +div.mainbox.browse.gamelist table tr td.tc2 {width:100px;} +div.mainbox.browse.gamelist table tr td.tc3 {width:80px;} +div.mainbox.browse.gamelist table tr td.tc4 {width:80px;} + +div.mainbox.browse.kfstats table tr td, +div.mainbox.browse.kfstats table thead tr td {padding-left:0; padding-right:0;} +div.mainbox.browse.kfstats table tr td.tc1 {width:30px;} +div.mainbox.browse.kfstats table tr td.tc2 {width:19px;} +div.mainbox.browse.kfstats table tr td.tc3 {width:130px;text-align:left} +div.mainbox.browse.kfstats table tr td.tc5, +div.mainbox.browse.kfstats table tr td.tc7, +div.mainbox.browse.kfstats table tr td.tc12, +div.mainbox.browse.kfstats table tr td.tc13 {width:85px;} + +div.mainbox.browse.frontpage table tr td.tc1 {text-align:left} +div.mainbox.browse.frontpage table tr td.tc2 {width:70px;} +div.mainbox.browse.frontpage table tr td.tc3 {width:80px;} + +div.mainbox.browse.frontmasterlist table tr td.tc2 {text-align:left} +div.mainbox.browse.frontmasterlist table tr td.tc3 {width:80px;} + +/* individual pages */ +div.mainbox table.fancytable { + table-layout:fixed; + border-collapse:collapse; + border:1px solid #000; + margin:20px auto 20px auto; + width:70%; + box-shadow: 10px 10px 5px #999; + background: #999; + } +div.mainbox table.fancytable tr th { + font-weight:bold; + color: #000; +} +div.mainbox table.fancytable tr th, +div.mainbox table.fancytable tr td { + vertical-align:top; + text-align:left; + padding:6px 12px; + border:1px solid #000; + } +div.mainbox table.fancytable.addresses .tc1 {width:160px} +div.mainbox table.fancytable.addresses .tc2 {width:120px;text-align:center;} +div.mainbox table.fancytable.addresses .tc3 {width:120px;text-align:center;} + +div.mainbox.contact{ + width:605px; + } +div.mainbox.contact img { + float:left; + height:120px; + width:105px; + margin:10px 30px 10px 10px; + border: 1px solid #000; + } +div.mainbox.contact table { + margin: 10px 5px 20pt 20px; + border-spacing: 0; + width:450px; + } +div.mainbox.contact table tr th{ + padding: 2px 20px 0 0; + border-bottom: 1px solid #555; + } +div.mainbox.contact table tr td{ + padding: 2px 20px 0 10px; + border-bottom: 1px solid #222; + vertical-align:top + } +div.mainbox.contact table tr td ul { + list-style: none; + margin:0 + } +div.mainbox.contact table tr th.tc1 {width:150px;} +div.mainbox.contact table tr th.tc2 {width:300px} + +/* individual images */ +.kfstatsimg{ + float:right; + margin: -20px 5px 0 15px; + background: #999; + padding: 6px; + border: 1px solid #222; + } +.kfstatsimg img{ + border: 1px solid #555; + width: 300px; + } + +/* IV main div*/ +#ivview { + position: absolute; + top: -500px; + background: #999; + border: 1px solid #000; + padding: 20px; + text-align: center; + } +#ivview img { + cursor: pointer; + border: 1px solid #000; + } +#ivclose { + float: right; + padding-left: 10px; + } +#ivnext { + padding-left: 5px; + } +#ivprev { + padding-right: 5px; + } +#ivfull { + float: left; + padding-right: 10px; + } +#ivimgload{ + display:block; + position:absolute; + left:-500px; + top:-50px; + width:100px; + padding:3px; + text-align: center; + border: 1px solid #000; + color: #000; + background: #ccc; + } diff --git a/util/masterwebinterface.pl b/util/masterwebinterface.pl new file mode 100755 index 0000000..7abee70 --- /dev/null +++ b/util/masterwebinterface.pl @@ -0,0 +1,38 @@ +#!/usr/bin/perl +package MasterWebInterface; +use strict; +use warnings; +use Cwd 'abs_path'; + +our $ROOT; +BEGIN { ($ROOT = abs_path $0) =~ s{/util/masterwebinterface.pl$}{}; } +use lib $ROOT.'/lib'; +use TUWF; + +our(%O, %S, @login); +require "$ROOT/data/webinterface-config.pl"; + +#add %S from web-config.pl to OBJ +$TUWF::OBJ->{$_} = $S{$_} for (keys %S); + +# TUWF options +TUWF::set( + logfile => "$ROOT/log/MasterWebInterface-TUWF.log", + mail_from => '<noreply@333networks.com>', + db_login => ['dbi:Pg:dbname=devmasterserver', 'unrealmaster', 'unrealmasterpassword'], + validate_templates => { # input templates + page => { template => 'uint', max => 1000 }, + }, + xml_pretty => 0, + log_queries => 1, + debug => 1, +); + +# load master page libs +TUWF::load_recursive('MasterWebInterface::Handler', + 'MasterWebInterface::Util', + 'MasterWebInterface::Database::Pg', + #'MasterWebInterface::Database::sqlite', + ); # Do not forget to choose the database type here! +#and let's roll! +TUWF::run(); |
