#!/usr/bin/perl use strict; use warnings; use Dancer2; use Socket; use DBI; use File::Fetch; use PerlIO::gzip; use LWP::UserAgent; use HTTP::Request; use Data::Validate::IP qw(is_ipv4); use Data::Dumper; ### README # to install needed modules on debian: # * apt-get install libperlio-gzip-perl libdancer2-perl libdbi-perl libdbd-sqlite3-perl libdata-validate-ip-perl ### vars my $dbfile = 'xtrd.db'; my $ip2asn_csv_url = 'http://iptoasn.com/data/ip2asn-v4-u32.tsv.gz'; my $server_protocol_version = 3; my $min_client_version = 3; set port => 12110; ### connect to database my $dbh = DBI->connect("dbi:SQLite:dbname=$dbfile","",""); $dbh->do("PRAGMA cache_size = 800000"); ### create ip2asn database & table if needed and fill with ip information if(create_db_table($dbh,"ip2asn","ip2asn( ip_from INTEGER, ip_to INTEGER, ip_as INTEGER)","ip2asnIndex ON ip2asn(ip_from,ip_to);")) { get_ipas_data($dbh,$ip2asn_csv_url); } ### 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; }; put '/v3/server/add/:ip/:port/:state' => sub { my $ip = route_parameters->get('ip'); my $port = route_parameters->get('port'); my $state = route_parameters->get('state'); ### check input if(!is_ipv4($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,$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 $ua = LWP::UserAgent->new(timeout => 2); my $req = HTTP::Request->new("GET", $url); my $response = $ua->request($req); my @return_code = ($response->is_success, $response->decoded_content); return @return_code; } sub add_server { my ($dbh,$ip,$port,$status) = @_; my $id = get_server_id($dbh,$ip,$port); if($id ne "0") { # update server status set_server_status($dbh,$id,$status); } else { # create my $resolve = gethostbyname($ip); my $a_record = inet_ntoa($resolve); my $as = find_as($dbh,$a_record); 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 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 get_ipas_data { my ($dbh,$ip2asn_csv_url) = @_; my $ff = File::Fetch->new(uri => $ip2asn_csv_url); my $temp_file = $ff->fetch( to => '/tmp' ); open(FILE,"<:gzip",$temp_file) or die "Can't open file: $!"; my @data = ; close(FILE); $dbh->do('begin'); my $max_commit = 10000; my $inserted = 0; my $array_size = @data; my $last_p = -1; print "loading data...\n"; foreach my $line (@data) { chomp($line); if($line=~/^(\d+)\s+(\d+)\s+(\d+)/) { if($3 eq "0") { next; } #my %temp_hash = ('from' => $1, 'to' => $2, 'as' => $3); #push(@ipaslist,\%temp_hash); my $insert_cmd .= "INSERT INTO ip2asn VALUES ('".$1."','".$2."','".$3."');"; my $insert_sth = $dbh->prepare($insert_cmd); $inserted += $insert_sth->execute(); # print status for console users my $p = int($inserted * 100 / $array_size); if($p != $last_p) { print $p % 10 ? "" : "$p%\n"; } $last_p = $p; # only commit every $max_commit statements (it's faster) unless ($inserted % $max_commit) { $dbh->do('commit'); $dbh->do('begin'); } } } $dbh->do('commit'); unlink($temp_file); print "data prepared for searches!\n"; } sub find_as { my ($dbh, $ip) = @_; my $ip_id = unpack("N", inet_aton($ip)); my $get_as_cmd = "SELECT ip_as FROM ip2asn WHERE ip_from < '".$ip_id."' AND ip_to > '".$ip_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 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; } }