375 lines
12 KiB
Perl
375 lines
12 KiB
Perl
#!/usr/bin/perl
|
|
|
|
use strict;
|
|
use warnings;
|
|
use Dancer2;
|
|
use Socket;
|
|
use DBI;
|
|
use File::Fetch;
|
|
use HTTP::Tiny;
|
|
use HTTP::Request;
|
|
use Data::Validate::Domain qw(is_domain);
|
|
use Data::Validate::IP qw(is_public_ipv4 is_public_ipv6);
|
|
use MaxMind::DB::Reader;
|
|
use Archive::Tar;
|
|
use Data::Dumper;
|
|
|
|
|
|
### README #########################################################################################################################################
|
|
# This is the server for the XTR (XiCoN Trace Route). It's more of a master browser for the clients, so an web frontend can get a list of all the
|
|
# available clients combined from one auto-updated source. It opens a port and listening to HTTP requests (GET and PUT). It can list all online and
|
|
# offline clients. It also accepts new client entries via a PUT request (target client address in database has to be the request ip).
|
|
#
|
|
# The server keeps the list of clients up2date with checking their availablity with each request of all online clients.
|
|
# For now, only IPv4 clients are supported. This will change in the future.
|
|
#
|
|
# This software uses the MaxMind GeoLite2 database (https://www.maxmind.com) to determine the AS for each hop. To lookup the AS information the script
|
|
# uses the MaxMind perl module which uses the MaxMind DB library.
|
|
#
|
|
#
|
|
# This product includes GeoLite2 ASN data created by MaxMind, available from <a href="http://www.maxmind.com">http://www.maxmind.com</a>.
|
|
######################################################################################################################################################
|
|
|
|
|
|
### CHANGELOG ######################################################################################################################################
|
|
# v0.5 (2020-04-14)
|
|
# * FIX: Maxmind now allows redistributing GeoLite2-ASN database if requirements are fulfilled (https://git.xicon.eu/xicon/xtr/issues/3)
|
|
# * ADD: Maxmind's TOS added as start-up message
|
|
#
|
|
# v0.4 (2019-08-22)
|
|
# * ADD: added better documentation
|
|
# * ADD: list of API commands via API
|
|
# * FIX: replaced bug-driven LWP::* with HTTP::Tiny
|
|
# * FIX: limited adding of clients to source ip of target entry (only add a client which you are coming from)
|
|
######################################################################################################################################################
|
|
|
|
|
|
### INSTALL ########################################################################################################################################
|
|
# This is a standalone software which usually runs in the "foreground". Starting it in a screen session or via init.d/systemd/rc.d is
|
|
# highly recommended. For testing, starting this script in a screen session is also fine.
|
|
#
|
|
# Either install all listed modules with "cpan -i <module>" or use your system's package manager (apt, yum, yast).
|
|
#
|
|
# On Debian just install there packages:
|
|
# screen
|
|
# libperlio-gzip-perl
|
|
# libdancer2-perl
|
|
# libdbi-perl
|
|
# libdbd-sqlite3-perl
|
|
# libdata-validate-ip-perl
|
|
# build-essential
|
|
# cpanminus
|
|
# libmaxminddb0
|
|
# libmaxminddb-dev
|
|
#
|
|
# After that, execute this command to install maxmind database reader module:
|
|
# cpanm -q -n MaxMind::DB::Reader::XS
|
|
#
|
|
######################################################################################################################################################
|
|
|
|
|
|
### VARS #############################################################################################################################################
|
|
my $dbfile = 'xtrd.db';
|
|
my $max_dbfile = 'GeoLite2-ASN.mmdb';
|
|
my $max_db_source = 'http://xtr.xicon.eu/GeoLite2-ASN.tar.gz';
|
|
my $server_protocol_version = 3;
|
|
my $min_client_version = 4;
|
|
set port => 12110;
|
|
######################################################################################################################################################
|
|
|
|
|
|
|
|
|
|
get_maxmind_db($max_db_source, $max_dbfile);
|
|
|
|
### connect to database
|
|
my $maxmind_reader = MaxMind::DB::Reader->new( file => $max_dbfile );
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
######################################################################################################################################################
|
|
### NOTHING TO CHANGE BELOW HERE ###################################################################################################################
|
|
######################################################################################################################################################
|
|
|
|
### connect to database
|
|
my $dbh = DBI->connect("dbi:SQLite:dbname=$dbfile","","");
|
|
|
|
|
|
### create server database & table if needed
|
|
create_db_table($dbh,"server","server( server_ip TEXT, server_port INTEGER, server_as INTEGER, server_status INTEGER)","");
|
|
|
|
|
|
|
|
### hook for HTTP "security"
|
|
hook 'before' => sub {
|
|
header 'Access-Control-Allow-Origin' => '*';
|
|
};
|
|
|
|
### main get route
|
|
get '/v3/server/list/online' => sub {
|
|
refresh_server_status($dbh,"1");
|
|
my $server = get_server($dbh,"1");
|
|
return encode_json $server;
|
|
};
|
|
|
|
get '/v3/server/list/offline' => sub {
|
|
my $server = get_server($dbh,"0");
|
|
return encode_json $server;
|
|
};
|
|
|
|
get '/v3/server/list/all' => sub {
|
|
my $server = get_server($dbh,"");
|
|
return encode_json $server;
|
|
};
|
|
|
|
get '/v3/server/info/version' => sub {
|
|
return $server_protocol_version;
|
|
};
|
|
|
|
get '/v3/server/info/commands' => sub {
|
|
my $commands = "/v".$server_protocol_version."/server/list/online\n";
|
|
$commands .= "/v".$server_protocol_version."/server/list/offline\n";
|
|
$commands .= "/v".$server_protocol_version."/server/list/all\n";
|
|
$commands .= "/v".$server_protocol_version."/server/add/<ip>/<port>/<state>\n";
|
|
$commands .= "/v".$server_protocol_version."/server/info/version\n";
|
|
$commands .= "/v".$server_protocol_version."/server/info/commands\n";
|
|
return $commands;
|
|
};
|
|
|
|
put '/v3/server/add/:ip/:port/:state' => sub {
|
|
### collect parameters
|
|
my $ip = route_parameters->get('ip');
|
|
my $port = route_parameters->get('port');
|
|
my $state = route_parameters->get('state');
|
|
|
|
### get client ip
|
|
my $client_ip = request->address;
|
|
my $request_ffa = request->forwarded_for_address;
|
|
if(is_public_ipv4($request_ffa)) { $client_ip = $request_ffa; }
|
|
|
|
### check input
|
|
if(!is_public_ipv4($ip) || $client_ip ne $ip) { status('bad_request'); return "invalid IP address"; }
|
|
if(!($port > 0 and $port < 65536)) { status('bad_request'); return "invalid port"; }
|
|
if(!($state >= 0 and $state < 256)) { status('bad_request'); return "invalid status"; }
|
|
|
|
my $result = add_server($dbh,$maxmind_reader,$ip,$port,$state);
|
|
return "OK";
|
|
};
|
|
|
|
### start the loop
|
|
start;
|
|
|
|
### if you reach this, you're done
|
|
exit;
|
|
|
|
sub check_server_status
|
|
{
|
|
my ($hostname,$port) = @_;
|
|
my $url = "http://".$hostname.":".$port."/v3/client/info/version";
|
|
my $httptiny = HTTP::Tiny->new("timeout" => 2);
|
|
my $response = $httptiny->request("GET",$url);
|
|
my @return_code = ($response->{'success'}, $response->{'content'});
|
|
return @return_code;
|
|
}
|
|
|
|
sub add_server
|
|
{
|
|
my ($dbh,$maxmind_reader,$ip,$port,$status) = @_;
|
|
|
|
|
|
# prepare data
|
|
my $resolve = gethostbyname($ip);
|
|
my $a_record = inet_ntoa($resolve);
|
|
my $as = 0;
|
|
my $as_name = " ";
|
|
my $as_data = find_as($maxmind_reader, $a_record);
|
|
if(defined($as_data->{'autonomous_system_number'})) { $as = $as_data->{'autonomous_system_number'}; }
|
|
if(defined($as_data->{'autonomous_system_organization'})) { $as_name = $as_data->{'autonomous_system_organization'}; }
|
|
|
|
my $id = get_server_id($dbh,$ip,$port);
|
|
if($id ne "0")
|
|
{
|
|
# get server as from database
|
|
my $db_as = get_server_as($dbh,$id);
|
|
|
|
# update server status
|
|
set_server_status($dbh,$id,$status);
|
|
|
|
if($as ne $db_as)
|
|
{
|
|
set_server_as($dbh,$id,$as);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
my $add_server_cmd = "INSERT INTO server VALUES ('".$ip."','".$port."','".$as."','".$status."');";
|
|
my $add_server_sth = $dbh->prepare($add_server_cmd);
|
|
$add_server_sth->execute();
|
|
}
|
|
}
|
|
|
|
sub set_server_status
|
|
{
|
|
my ($dbh,$id,$status) = @_;
|
|
|
|
my $set_status_cmd = "UPDATE server set server_status = '".$status."' WHERE _rowid_ = '".$id."';";
|
|
my $set_status_sth = $dbh->prepare($set_status_cmd);
|
|
$set_status_sth->execute();
|
|
}
|
|
|
|
sub get_server_id
|
|
{
|
|
my ($dbh,$ip,$port) = @_;
|
|
|
|
my $get_id_cmd = "SELECT _rowid_ FROM server WHERE server_ip = '".$ip."' AND server_port = '".$port."';";
|
|
my $get_id_sth = $dbh->prepare($get_id_cmd);
|
|
$get_id_sth->execute();
|
|
my $result = $get_id_sth->fetch;
|
|
return $result->[0] || 0;
|
|
}
|
|
|
|
sub set_server_as
|
|
{
|
|
my ($dbh,$id,$as) = @_;
|
|
|
|
my $set_as_cmd = "UPDATE server set server_as = '".$as."' WHERE _rowid_ = '".$id."';";
|
|
my $set_as_sth = $dbh->prepare($set_as_cmd);
|
|
$set_as_sth->execute();
|
|
}
|
|
|
|
sub get_server_as
|
|
{
|
|
my ($dbh,$id) = @_;
|
|
|
|
my $get_as_cmd = "SELECT server_as FROM server WHERE _rowid_ = '".$id."';";
|
|
my $get_as_sth = $dbh->prepare($get_as_cmd);
|
|
$get_as_sth->execute();
|
|
my $result = $get_as_sth->fetch;
|
|
return $result->[0] || 0;
|
|
}
|
|
|
|
sub refresh_server_status
|
|
{
|
|
my ($dbh,$status) = @_;
|
|
my $temp_data = get_server($dbh,$status);
|
|
|
|
### check all servers and update status
|
|
foreach my $item (@$temp_data)
|
|
{
|
|
my ($success,$version) = check_server_status($item->[0],$item->[1]);
|
|
if($success)
|
|
{
|
|
if($version >= $min_client_version)
|
|
{
|
|
my $id = get_server_id($dbh, $item->[0],$item->[1]);
|
|
set_server_status($dbh, $id, "1");
|
|
}
|
|
else
|
|
{
|
|
my $id = get_server_id($dbh, $item->[0],$item->[1]);
|
|
set_server_status($dbh, $id, "0");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
my $id = get_server_id($dbh, $item->[0],$item->[1]);
|
|
set_server_status($dbh, $id, "0");
|
|
}
|
|
}
|
|
}
|
|
|
|
sub get_server
|
|
{
|
|
my ($dbh,$status) = @_;
|
|
|
|
my $get_server_cmd = "";
|
|
|
|
if($status eq "")
|
|
{
|
|
$get_server_cmd = "SELECT * FROM server;";
|
|
}
|
|
else
|
|
{
|
|
$get_server_cmd = "SELECT * FROM server WHERE server_status = '".$status."';";
|
|
}
|
|
|
|
my $get_server_sth = $dbh->prepare($get_server_cmd);
|
|
$get_server_sth->execute();
|
|
my $result = $get_server_sth->fetchall_arrayref;
|
|
|
|
return $result;
|
|
}
|
|
|
|
sub find_as
|
|
{
|
|
my ($maxmind_reader, $ip) = @_;
|
|
my $record = $maxmind_reader->record_for_address($ip);
|
|
return $record;
|
|
}
|
|
|
|
sub get_maxmind_db
|
|
{
|
|
my ($max_db_source,$max_dbfile) = @_;
|
|
|
|
my $ff = File::Fetch->new(uri => $max_db_source);
|
|
my $temp_file = $ff->fetch( to => '/tmp' );
|
|
|
|
my $tar = Archive::Tar->new;
|
|
$tar->read($temp_file);
|
|
|
|
my @files = $tar->get_files();
|
|
foreach my $file (@files)
|
|
{
|
|
if($file->{'name'}=~/${max_dbfile}$/)
|
|
{
|
|
open(FILE,">",$max_dbfile) or die "Can't open file for writing: $!";
|
|
print FILE $file->{'data'};
|
|
close(FILE);
|
|
}
|
|
}
|
|
$tar->clear;
|
|
unlink($temp_file);
|
|
}
|
|
|
|
sub create_db_table
|
|
{
|
|
my ($dbh,$table,$schema,$schema_index) = @_;
|
|
|
|
my $check_table_cmd = "SELECT name FROM sqlite_master WHERE type='table' AND name='".$table."';";
|
|
my $check_table_sth = $dbh->prepare($check_table_cmd);
|
|
$check_table_sth->execute();
|
|
if(!$check_table_sth->fetch)
|
|
{
|
|
print "creating table '".$table."'...\n";
|
|
my $create_table_cmd = "CREATE TABLE ".$schema;
|
|
my $create_table_sth = $dbh->prepare($create_table_cmd);
|
|
$create_table_sth->execute();
|
|
if($schema_index ne "")
|
|
{
|
|
my $create_index_cmd = "CREATE INDEX ".$schema_index;
|
|
my $create_index_sth = $dbh->prepare($create_index_cmd);
|
|
$create_index_sth->execute();
|
|
}
|
|
return 1;
|
|
}
|
|
else
|
|
{
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
sub print_maxmind_tos
|
|
{
|
|
print qq{ --------------------------------------------------------------------------------------------------------------------------------------- };
|
|
print "\n";
|
|
print qq{ This product includes GeoLite2 ASN data created by MaxMind, available from <a href="http://www.maxmind.com">http://www.maxmind.com</a>. };
|
|
print "\n";
|
|
print qq{ --------------------------------------------------------------------------------------------------------------------------------------- };
|
|
print "\n";
|
|
print "\n";
|
|
} |