diff options
Diffstat (limited to 'util/master')
-rw-r--r-- | util/master/index.html | 11 | ||||
-rw-r--r-- | util/master/list.js | 72 | ||||
-rwxr-xr-x | util/master/master.cgi | 257 | ||||
-rw-r--r-- | util/master/style.css | 14 |
4 files changed, 354 insertions, 0 deletions
diff --git a/util/master/index.html b/util/master/index.html new file mode 100644 index 000000000..63ee5c70f --- /dev/null +++ b/util/master/index.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <title>Minetest server list</title> + <link rel="stylesheet" href="style.css"/> + </head> + <body><div id="table"></div></body> +</html> +<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script> +<script src="list.js"></script> diff --git a/util/master/list.js b/util/master/list.js new file mode 100644 index 000000000..dcc30e091 --- /dev/null +++ b/util/master/list.js @@ -0,0 +1,72 @@ +function e(s) { + if (typeof s === "undefined") s = ''; + return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); //mc" +} +function human_time(t) { + var n = 's'; + if (!t || t < 0) t = 0; + var f = 0; + var s = parseInt((new Date().getTime() / 1000 - (t || 0))); + if (!s || s <= 0) s = 0; + if (s == 0) return 'now'; + if (s >= 60) { + s /= 60; + n = 'm'; + if (s >= 60) { + s /= 60; + n = 'h'; + f = 1; + if (s >= 24) { + s /= 24; + n = 'd'; + f = 1; + if (s >= 30) { + s /= 30; + n = 'M'; + f = 1; + if (s >= 12) { + s /= 12; + n = 'y'; + f = 1; + } + } + } + } + } + return ((f ? parseFloat(s).toFixed(1) : parseInt(s)) + n); +} +function success(r) { + if (!r || !r.list) return; + var h = '<table><tr><th>ip:port</th><th>clients, max</th><th>version</th><th>name</th><th>desc</th><th>flags</th><th>updated/started</th><th>ping</th></tr>'; + for (var i = 0; i < r.list.length; ++i) { + var s = r.list[i]; + if (!s) continue; + h += '<tr>'; + h += '<td>' + e(s.address) + ':' + e(s.port) + '</td>'; + h += '<td>' + e(s.clients) + (s.clients_max ? '/' + e(s.clients_max) : '') + (s.clients_top ? ', ' + s.clients_top : '') + '</td>'; + h += '<td>' + e(s.version) + '</td>'; + h += '<td>'; + if (s.url) h += '<a href="' + e(s.url) + '">'; + h += e(s.name || s.url); + if (s.url) h += '</a>'; + h += '</td>'; + h += '<td>' + e(s.description) + '</td>'; + h += '<td>' + e(s.password ? 'Pwd ' : '') + (s.creative ? 'Cre ' : '') + (s.damage ? 'Dmg ' : '') + (s.pvp ? 'Pvp ' : '') + (s.dedicated ? 'Ded ' : '') + '</td>'; + if (!s.time || s.time < 0) s.time = 0; + if (!s.start || s.start < 0) s.start = 0; + h += '<td>' + human_time(s.time) + (s.start ? '/' + human_time(s.start) : '') + '</td>'; + h += '<td>' + (s.ping ? parseFloat(s.ping).toFixed(3)*1000 : '') + '</td>'; + h += '</tr>'; + } + h += '</table>' + jQuery('#table').html(h); +} +function get() { + jQuery.ajax({ + url: 'list', + dataType: 'json', + success: success + }); + setTimeout(get, 60000); +} +get();
\ No newline at end of file diff --git a/util/master/master.cgi b/util/master/master.cgi new file mode 100755 index 000000000..b918876bd --- /dev/null +++ b/util/master/master.cgi @@ -0,0 +1,257 @@ +#!/usr/bin/perl + +=info +install: + cpan JSON JSON::XS + touch list_full list + chmod a+rw list_full list + +freebsd: + www/fcgiwrap www/nginx + +rc.conf.local: +nginx_enable="YES" +fcgiwrap_enable="YES" +fcgiwrap_user="www" + +nginx: + + location / { + index index.html; + } + location /announce { + fastcgi_pass unix:/var/run/fcgiwrap/fcgiwrap.sock; + fastcgi_param SCRIPT_FILENAME $document_root/master.cgi; + include fastcgi_params; + } + + +apache .htaccess: + AddHandler cgi-script .cgi + DirectoryIndex index.html + Options +ExecCGI +FollowSymLinks + Order allow,deny + <FilesMatch (\.(html?|cgi|fcgi|css|js|gif|png|jpe?g|ico)|(^)|\w+)$> + Allow from all + </FilesMatch> + Deny from all + + +=cut + +use strict; +no strict qw(refs); +use warnings "NONFATAL" => "all"; +no warnings qw(uninitialized); +use utf8; +use Socket; +use Time::HiRes qw(time sleep); +use IO::Socket::INET; +use JSON; +use Net::Ping; +our $root_path; +($ENV{'SCRIPT_FILENAME'} || $0) =~ m|^(.+)[/\\].+?$|; #v0w +$root_path = $1 . '/' if $1; +$root_path =~ s|\\|/|g; + +our %config = ( + #debug => 1, + list_full => $root_path . 'list_full', + list_pub => $root_path . 'list', + time_purge => 86400 * 30, + time_alive => 650, + source_check => 1, + ping_timeout => 3, + ping => 1, + mineping => 1, + pingable => 1, + trusted => [qw( 176.9.122.10 )], #masterserver self ip - if server on same ip with masterserver doesnt announced + #blacklist => [], # [qw(2.3.4.5 4.5.6.7 8.9.0.1), '1.2.3.4', qr/^10\.20\.30\./, ], # list, or quoted, ips, or regex +); +do($root_path . 'config.pl'); +our $ping = Net::Ping->new("udp", $config{ping_timeout}); +$ping->hires(); + +sub get_params_one(@) { + local %_ = %{ref $_[0] eq 'HASH' ? shift : {}}; + for (@_) { + tr/+/ /, s/%([a-f\d]{2})/pack 'H*', $1/gei for my ($k, $v) = /^([^=]+=?)=(.+)$/ ? ($1, $2) : (/^([^=]*)=?$/, /^-/); + $_{$k} = $v; + } + wantarray ? %_ : \%_; +} + +sub get_params(;$$) { #v7 + my ($string, $delim) = @_; + $delim ||= '&'; + read(STDIN, local $_ = '', $ENV{'CONTENT_LENGTH'}) if !$string and $ENV{'CONTENT_LENGTH'}; + local %_ = + $string + ? get_params_one split $delim, $string + : (get_params_one(@ARGV), map { get_params_one split $delim, $_ } split(/;\s*/, $ENV{'HTTP_COOKIE'}), $ENV{'QUERY_STRING'}, $_); + wantarray ? %_ : \%_; +} + +sub get_params_utf8(;$$) { + local $_ = &get_params; + utf8::decode $_ for %$_; + wantarray ? %$_ : $_; +} + +sub file_rewrite(;$@) { + local $_ = shift; + return unless open my $fh, '>', $_; + print $fh @_; +} + +sub file_read ($) { + open my $f, '<', $_[0] or return; + local $/ = undef; + my $ret = <$f>; + close $f; + return \$ret; +} + +sub read_json { + my $ret = {}; + eval { $ret = JSON->new->utf8->relaxed(1)->decode(${ref $_[0] ? $_[0] : file_read($_[0]) or \''} || '{}'); }; #'mc + warn "json error [$@] on [", ${ref $_[0] ? $_[0] : \$_[0]}, "]" if $@; + $ret; +} + +sub printu (@) { + for (@_) { + print($_), next unless utf8::is_utf8($_); + my $s = $_; + utf8::encode($s); + print($s); + } +} + +sub name_to_ip_noc($) { + my ($name) = @_; + unless ($name =~ /^\d+\.\d+\.\d+\.\d+$/) { + local $_ = (gethostbyname($name))[4]; + return ($name, 1) unless length($_) == 4; + $name = inet_ntoa($_); + } + return $name; +} + +sub float { + return ($_[0] < 8 and $_[0] - int($_[0])) + ? sprintf('%.' . ($_[0] < 1 ? 3 : ($_[0] < 3 ? 2 : 1)) . 'f', $_[0]) + : int($_[0]); + +} + +sub mineping ($$) { + my ($addr, $port) = @_; + warn "mineping($addr, $port)" if $config{debug}; + my $data; + my $time = time; + eval { + my $socket = IO::Socket::INET->new( + 'PeerAddr' => $addr, + 'PeerPort' => $port, + 'Proto' => 'udp', + 'Timeout' => $config{ping_timeout}, + ); + $socket->send("\x4f\x45\x74\x03\x00\x00\x00\x01"); + local $SIG{ALRM} = sub { die "alarm time out"; }; + alarm $config{ping_timeout}; + $socket->recv($data, POSIX::BUFSIZ) or die "recv: $!"; + alarm 0; + 1; # return value from eval on normalcy + } or return 0; + return 0 unless length $data; + $time = float(time - $time); + warn "recvd: ", length $data, " [$time]" if $config{debug}; + return $time; +} + +sub request (;$) { + my ($r) = @_; + $r ||= \%ENV; + my $param = get_params_utf8; + my $after = sub { + if ($param->{json}) { + my $j = {}; + eval { $j = JSON->new->decode($param->{json}) || {} }; + $param->{$_} = $j->{$_} for keys %$j; + delete $param->{json}; + } + if (%$param) { + s/^false$// for values %$param; + $param->{ip} = $r->{REMOTE_ADDR}; + for (@{$config{blacklist}}) { + return if $param->{ip} ~~ $_; + } + $param->{address} ||= $param->{ip}; + if ($config{source_check} and name_to_ip_noc($param->{address}) ne $param->{ip} and !($param->{ip} ~~ $config{trusted})) { + warn("bad address [$param->{address}] ne [$param->{ip}]") if $config{debug}; + return; + } + $param->{port} ||= 30000; + $param->{key} = "$param->{ip}:$param->{port}"; + $param->{off} = time if $param->{action} ~~ 'delete'; + + if ($config{ping} and $param->{action} ne 'delete') { + if ($config{mineping}) { + $param->{ping} = mineping($param->{ip}, $param->{port}); + } else { + $ping->port_number($param->{port}); + $ping->service_check(0); + my ($pingret, $duration, $ip) = $ping->ping($param->{address}); + if ($ip ne $param->{ip} and !($param->{ip} ~~ $config{trusted})) { + warn "strange ping ip [$ip] != [$param->{ip}]" if $config{debug}; + return if $config{source_check} and !($param->{ip} ~~ $config{trusted}); + } + $param->{ping} = $duration if $pingret; + warn " PING t=$config{ping_timeout}, $param->{address}:$param->{port} = ( $pingret, $duration, $ip )" if $config{debug}; + } + } + my $list = read_json($config{list_full}) || {}; + warn "readed[$config{list_full}] list size=", scalar @{$list->{list}}; + my $listk = {map { $_->{key} => $_ } @{$list->{list}}}; + my $old = $listk->{$param->{key}}; + $param->{time} = $old->{time} if $param->{off}; + $param->{time} ||= int time; + $param->{start} = $param->{action} ~~ 'start' ? $param->{time} : $old->{start} || $param->{time}; + delete $param->{start} if $param->{off}; + $param->{first} ||= $old->{first} || $old->{time} || $param->{time}; + $param->{clients_top} = $old->{clients_top} if $old->{clients_top} > $param->{clients}; + $param->{clients_top} ||= $param->{clients} || 0; + delete $param->{action}; + $listk->{$param->{key}} = $param; + $list->{list} = [grep { $_->{time} > time - $config{time_purge} } values %$listk]; + file_rewrite($config{list_full}, JSON->new->encode($list)); + warn "writed[$config{list_full}] list size=", scalar @{$list->{list}}; + $list->{list} = [ + sort { $b->{clients} <=> $a->{clients} || $a->{start} <=> $b->{start} } + grep { $_->{time} > time - $config{time_alive} and !$_->{off} and (!$config{ping} or !$config{pingable} or $_->{ping}) } + @{$list->{list}} + ]; + file_rewrite($config{list_pub}, JSON->new->encode($list)); + warn "writed[$config{list_pub}] list size=", scalar @{$list->{list}}; + } + }; + return [200, ["Content-type", "application/json"], [JSON->new->encode({})]], $after; +} + +sub request_cgi { + my ($p, $after) = request(@_); + shift @$p; + printu join "\n", map { join ': ', @$_ } shift @$p; + printu "\n\n"; + printu join '', map { join '', @$_ } @$p; + if (fork) { + unless ($config{debug}) { + close STDOUT; + close STDERR; + } + } else { + $after->() if ref $after ~~ 'CODE'; + } +} +request_cgi() unless caller; diff --git a/util/master/style.css b/util/master/style.css new file mode 100644 index 000000000..638b51f80 --- /dev/null +++ b/util/master/style.css @@ -0,0 +1,14 @@ +table { + max-width: 100%; + background-color: transparent; + border-collapse: collapse; + border-spacing: 0; +} + +td, th { + border: 1px solid gray; +} + +div#table table { + width: 100%; +}
\ No newline at end of file |