#!/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 http://www.maxmind.com. ###################################################################################################################################################### ### 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 " 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///\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 http://www.maxmind.com. }; print "\n"; print qq{ --------------------------------------------------------------------------------------------------------------------------------------- }; print "\n"; print "\n"; }