# NOTES # - test REST with: wget -O - http://localhost:3103/locations/list # - performance of GeoIP2::Database::Reader is about the same # as MaxMind::DB::Reader (48s), however it gets much better # with # apt-get install libmaxminddb-dev # cpanm install MaxMind::DB::Reader::XS # which uses a C implementation # (1s, with or without the /24 merging opt.) # currently, the list creation + transfer is about 340 ms # BUGS # - the unique_longlat means that only one IP is shown, # we could give the list instead (tooltip) # - Firefox mobile does not display the footer # TODO # BASED-ON # ../../compta/tests/REST # LICENCE # (C) Marc Schaefer 2023 # A-GPL use Mojolicious::Lite; use Mojo::JSON qw(encode_json); use Try::Tiny; use Time::HiRes qw(gettimeofday tv_interval); use MCE::Map; plugin 'RenderFile'; use MaxMind::DB::Reader; my $reader = MaxMind::DB::Reader->new(file => '../db/GeoLite2-City.mmdb') or die("reader"); my $ip_file = 'ip-list'; my $content; my $content_stamp = 0; sub read_list { my $file; if (open($file, "<", $ip_file)) { $content_stamp = file_stamp($file); my @ips = <$file>; chomp(@ips); print STDERR "loaded ", scalar(@ips), " IPs\n"; close($file); # err. ign. return @ips; } else { print STDERR $0, ": cannot open file: ", $!, "\n"; return (); } # NOT REACHED } sub get_location { my ($ip, $cache) = @_; my $location; # very slight optimization my $subnet = $ip; $subnet =~ s/\.\d+$//; $subnet .= '.0'; if (exists($cache->{$subnet})) { $location = $cache->{$subnet}; } else { try { my $record = $reader->record_for_address($subnet); if (defined($record)) { $location = $record->{location}; } } catch { print STDERR $0, ": GeoAPI error caught: ", $_, "\n"; }; # also cache undef $cache->{$subnet} = $location; } if (defined($location)) { return [ $ip, $location->{longitude}, $location->{latitude} ]; } else { print STDERR $0, ": location for ", $ip, " not found.\n"; return []; } # NOT REACHED } sub file_stamp { my ($file) = @_; my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size, $atime,$mtime,$ctime,$blksize,$blocks) = stat($file); return $mtime; } sub unique_longlat { my @longlats = @_; my @out; my %seen; foreach my $t (@longlats) { my ($ip, $long, $lat) = @{$t}; if (defined($long) && defined($lat)) { my $str = $long . "/" . $lat; if (!exists($seen{$str})) { push(@out, $t); $seen{$str} = undef; } } } return @out; } $| = 1; # REST / JSON web service # (with some internal caching as long as the ip-list doesn't change, and # additional per /24 caching; with the new API, none of those two # caches are really required now: the %cache spares about 100 ms # and the $content cache 300 ms) get '/locations/list' => sub { my ($self) = @_; my $stamp = file_stamp($ip_file); if (!defined($content) || ($content_stamp < $stamp)) { my $t = [gettimeofday]; my %cache; my @points = read_list; # MCE::Map will speed about by a factor of 4 # (I guess the cache will not be shared, but it is still useful) my @unique_points = unique_longlat mce_map { get_location($_, \%cache) } @points; $content = encode_json({ points => \@unique_points, count => scalar(@points), unique => scalar(@unique_points), stamp => $stamp }); print STDERR "duration: ", tv_interval($t), "\n"; } $self->render(data => $content, format=> 'json'); }; get '/' => sub { my ($self) = @_; $self->render('index'); }; get '/source' => sub { my $self = shift; $self->render_file('filepath' => "geoip2-api.pl"); }; app->start; # Templates follow below __DATA__ @@ index.html.ep % my $title = "OpenLayers demo: ALPHANET is blocking those abusers"; <%= $title %>
War
Ongoing
Aggravated
Normal
Peace

<%= $title %>

@@ attacks.js function init() { // set mark (and tooltip as IP address) at location // https://stackoverflow.com/questions/54907386/add-a-text-next-to-the-point-open-layer function setMarkLocation(l) { let ip = l.shift(); this.push(new ol.Feature({ geometry: new ol.geom.Point(ol.proj.fromLonLat(l)), name: ip })); } let tooltip = document.getElementById('tooltip'); let overlay = new ol.Overlay({ element: tooltip, offset: [10, 0], positioning: 'bottom-left' }); // https://stackoverflow.com/questions/35848697/openlayers-3-how-to-make-tootlip-for-feature function displayToolTip(e) { let feature = getLastFeatureAtPixel(e); tooltip.style.display = feature ? '' : 'none'; if (feature) { overlay.setPosition(e.coordinate); tooltip.innerText = feature.get('name'); } } function followLink(e) { let feature = getLastFeatureAtPixel(e); if (feature) { window.open("https://www.abuseipdb.com/check/" + encodeURI(feature.get('name'))); } } function getLastFeatureAtPixel(e) { let feature; map.forEachFeatureAtPixel(e.pixel, function(f) { feature = f; }); return feature; } // get the locations from REST web service function locations() { const rq = new XMLHttpRequest(); rq.open('GET', '/locations/list', true); rq.responseType = 'json'; rq.onload = function() { let status = rq.status; if (status == 200) { let stamp = new Date(rq.response.stamp * 1000); document.getElementById("stamp").innerText = 'IP count: ' + rq.response.count + ' (unique by location: ' + rq.response.unique + ')' + ' stamp: ' + stamp.toISOString() + ' (' + stamp.toLocaleString() + ')'; let iconFeatures = []; rq.response.points.forEach(setMarkLocation, iconFeatures); map.addLayer(new ol.layer.Vector({ source: new ol.source.Vector({ features: iconFeatures }) })); map.addOverlay(overlay); map.on('pointermove', displayToolTip); map.on('click', followLink); } // else ignored }; rq.send(); } let map = new ol.Map({ target: 'map', layers: [ new ol.layer.Tile({ source: new ol.source.OSM() }) ], view: new ol.View({ center: //ol.proj.fromLonLat([7.44744, 46.94809]), // Bern ol.proj.fromLonLat([0, 15]), zoom: 1 }) }); locations(); }