mirror of
https://git.proxmox.com/git/proxmox-spamassassin
synced 2025-04-28 12:19:37 +00:00
900 lines
27 KiB
Perl
900 lines
27 KiB
Perl
# <@LICENSE>
|
|
# Licensed to the Apache Software Foundation (ASF) under one or more
|
|
# contributor license agreements. See the NOTICE file distributed with
|
|
# this work for additional information regarding copyright ownership.
|
|
# The ASF licenses this file to you under the Apache License, Version 2.0
|
|
# (the "License"); you may not use this file except in compliance with
|
|
# the License. You may obtain a copy of the License at:
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
# </@LICENSE>
|
|
|
|
=head1 NAME
|
|
|
|
Mail::SpamAssassin::GeoDB - unified interface for geoip modules
|
|
|
|
Plugins need to signal SA main package the modules they want loaded
|
|
|
|
package Mail::SpamAssassin::Plugin::MyPlugin;
|
|
sub new {
|
|
...
|
|
$self-E<gt>{main}-E<gt>{geodb_wanted}-E<gt>{country} = 1;
|
|
$self-E<gt>{main}-E<gt>{geodb_wanted}-E<gt>{isp} = 1;
|
|
)
|
|
|
|
(internal stuff still subject to change)
|
|
|
|
=cut
|
|
|
|
package Mail::SpamAssassin::GeoDB;
|
|
|
|
use strict;
|
|
use warnings;
|
|
# use bytes;
|
|
use re 'taint';
|
|
|
|
use Socket;
|
|
use version 0.77;
|
|
|
|
our @ISA = qw();
|
|
|
|
use Mail::SpamAssassin::Constants qw(:ip);
|
|
use Mail::SpamAssassin::Logger;
|
|
|
|
my @geoip_default_path = qw(
|
|
/usr/local/share/GeoIP
|
|
/usr/share/GeoIP
|
|
/var/lib/GeoIP
|
|
/opt/share/GeoIP
|
|
);
|
|
|
|
# load order (city contains country, isp contains asn)
|
|
my @geoip_types = qw( city country isp asn );
|
|
|
|
# v6 is not needed, automatically tries *v6.dat also
|
|
my %geoip_default_files = (
|
|
'city' => ['GeoIPCity.dat','GeoLiteCity.dat'],
|
|
'country' => ['GeoIP.dat'],
|
|
'isp' => ['GeoIPISP.dat'],
|
|
'asn' => ['GeoIPASNum.dat'],
|
|
);
|
|
|
|
my %geoip2_default_files = (
|
|
'city' => ['GeoIP2-City.mmdb','GeoLite2-City.mmdb',
|
|
'dbip-city.mmdb','dbip-city-lite.mmdb'],
|
|
'country' => ['GeoIP2-Country.mmdb','GeoLite2-Country.mmdb',
|
|
'dbip-country.mmdb','dbip-country-lite.mmdb'],
|
|
'isp' => ['GeoIP2-ISP.mmdb','GeoLite2-ISP.mmdb'],
|
|
'asn' => ['GeoIP2-ASN.mmdb','GeoLite2-ASN.mmdb'],
|
|
);
|
|
|
|
my %country_to_continent = (
|
|
'AP'=>'AS','EU'=>'EU','AD'=>'EU','AE'=>'AS','AF'=>'AS','AG'=>'NA',
|
|
'AI'=>'NA','AL'=>'EU','AM'=>'AS','CW'=>'NA','AO'=>'AF','AQ'=>'AN',
|
|
'AR'=>'SA','AS'=>'OC','AT'=>'EU','AU'=>'OC','AW'=>'NA','AZ'=>'AS',
|
|
'BA'=>'EU','BB'=>'NA','BD'=>'AS','BE'=>'EU','BF'=>'AF','BG'=>'EU',
|
|
'BH'=>'AS','BI'=>'AF','BJ'=>'AF','BM'=>'NA','BN'=>'AS','BO'=>'SA',
|
|
'BR'=>'SA','BS'=>'NA','BT'=>'AS','BV'=>'AN','BW'=>'AF','BY'=>'EU',
|
|
'BZ'=>'NA','CA'=>'NA','CC'=>'AS','CD'=>'AF','CF'=>'AF','CG'=>'AF',
|
|
'CH'=>'EU','CI'=>'AF','CK'=>'OC','CL'=>'SA','CM'=>'AF','CN'=>'AS',
|
|
'CO'=>'SA','CR'=>'NA','CU'=>'NA','CV'=>'AF','CX'=>'AS','CY'=>'AS',
|
|
'CZ'=>'EU','DE'=>'EU','DJ'=>'AF','DK'=>'EU','DM'=>'NA','DO'=>'NA',
|
|
'DZ'=>'AF','EC'=>'SA','EE'=>'EU','EG'=>'AF','EH'=>'AF','ER'=>'AF',
|
|
'ES'=>'EU','ET'=>'AF','FI'=>'EU','FJ'=>'OC','FK'=>'SA','FM'=>'OC',
|
|
'FO'=>'EU','FR'=>'EU','FX'=>'EU','GA'=>'AF','GB'=>'EU','GD'=>'NA',
|
|
'GE'=>'AS','GF'=>'SA','GH'=>'AF','GI'=>'EU','GL'=>'NA','GM'=>'AF',
|
|
'GN'=>'AF','GP'=>'NA','GQ'=>'AF','GR'=>'EU','GS'=>'AN','GT'=>'NA',
|
|
'GU'=>'OC','GW'=>'AF','GY'=>'SA','HK'=>'AS','HM'=>'AN','HN'=>'NA',
|
|
'HR'=>'EU','HT'=>'NA','HU'=>'EU','ID'=>'AS','IE'=>'EU','IL'=>'AS',
|
|
'IN'=>'AS','IO'=>'AS','IQ'=>'AS','IR'=>'AS','IS'=>'EU','IT'=>'EU',
|
|
'JM'=>'NA','JO'=>'AS','JP'=>'AS','KE'=>'AF','KG'=>'AS','KH'=>'AS',
|
|
'KI'=>'OC','KM'=>'AF','KN'=>'NA','KP'=>'AS','KR'=>'AS','KW'=>'AS',
|
|
'KY'=>'NA','KZ'=>'AS','LA'=>'AS','LB'=>'AS','LC'=>'NA','LI'=>'EU',
|
|
'LK'=>'AS','LR'=>'AF','LS'=>'AF','LT'=>'EU','LU'=>'EU','LV'=>'EU',
|
|
'LY'=>'AF','MA'=>'AF','MC'=>'EU','MD'=>'EU','MG'=>'AF','MH'=>'OC',
|
|
'MK'=>'EU','ML'=>'AF','MM'=>'AS','MN'=>'AS','MO'=>'AS','MP'=>'OC',
|
|
'MQ'=>'NA','MR'=>'AF','MS'=>'NA','MT'=>'EU','MU'=>'AF','MV'=>'AS',
|
|
'MW'=>'AF','MX'=>'NA','MY'=>'AS','MZ'=>'AF','NA'=>'AF','NC'=>'OC',
|
|
'NE'=>'AF','NF'=>'OC','NG'=>'AF','NI'=>'NA','NL'=>'EU','NO'=>'EU',
|
|
'NP'=>'AS','NR'=>'OC','NU'=>'OC','NZ'=>'OC','OM'=>'AS','PA'=>'NA',
|
|
'PE'=>'SA','PF'=>'OC','PG'=>'OC','PH'=>'AS','PK'=>'AS','PL'=>'EU',
|
|
'PM'=>'NA','PN'=>'OC','PR'=>'NA','PS'=>'AS','PT'=>'EU','PW'=>'OC',
|
|
'PY'=>'SA','QA'=>'AS','RE'=>'AF','RO'=>'EU','RU'=>'EU','RW'=>'AF',
|
|
'SA'=>'AS','SB'=>'OC','SC'=>'AF','SD'=>'AF','SE'=>'EU','SG'=>'AS',
|
|
'SH'=>'AF','SI'=>'EU','SJ'=>'EU','SK'=>'EU','SL'=>'AF','SM'=>'EU',
|
|
'SN'=>'AF','SO'=>'AF','SR'=>'SA','ST'=>'AF','SV'=>'NA','SY'=>'AS',
|
|
'SZ'=>'AF','TC'=>'NA','TD'=>'AF','TF'=>'AN','TG'=>'AF','TH'=>'AS',
|
|
'TJ'=>'AS','TK'=>'OC','TM'=>'AS','TN'=>'AF','TO'=>'OC','TL'=>'AS',
|
|
'TR'=>'EU','TT'=>'NA','TV'=>'OC','TW'=>'AS','TZ'=>'AF','UA'=>'EU',
|
|
'UG'=>'AF','UM'=>'OC','US'=>'NA','UY'=>'SA','UZ'=>'AS','VA'=>'EU',
|
|
'VC'=>'NA','VE'=>'SA','VG'=>'NA','VI'=>'NA','VN'=>'AS','VU'=>'OC',
|
|
'WF'=>'OC','WS'=>'OC','YE'=>'AS','YT'=>'AF','RS'=>'EU','ZA'=>'AF',
|
|
'ZM'=>'AF','ME'=>'EU','ZW'=>'AF','AX'=>'EU','GG'=>'EU','IM'=>'EU',
|
|
'JE'=>'EU','BL'=>'NA','MF'=>'NA','BQ'=>'NA','SS'=>'AF','**'=>'**',
|
|
);
|
|
|
|
sub new {
|
|
my ($class, $conf) = @_;
|
|
$class = ref($class) || $class;
|
|
|
|
my $self = {};
|
|
bless ($self, $class);
|
|
|
|
$self->{cache} = ();
|
|
$self->init_database($conf || {});
|
|
$self;
|
|
}
|
|
|
|
sub init_database {
|
|
my ($self, $opts) = @_;
|
|
|
|
# Try city too if country wanted
|
|
$opts->{wanted}->{city} = 1 if $opts->{wanted}->{country};
|
|
# Try isp too if asn wanted
|
|
$opts->{wanted}->{isp} = 1 if $opts->{wanted}->{asn};
|
|
|
|
my $geodb_opts = {
|
|
'module' => $opts->{conf}->{module} || undef,
|
|
'dbs' => $opts->{conf}->{options} || undef,
|
|
'wanted' => $opts->{wanted} || undef,
|
|
'search_path' => defined $opts->{conf}->{geodb_search_path} ?
|
|
$opts->{conf}->{geodb_search_path} : \@geoip_default_path,
|
|
};
|
|
|
|
my ($db, $dbapi, $loaded);
|
|
|
|
## GeoIP2
|
|
if (!$db && (!$geodb_opts->{module} || $geodb_opts->{module} eq 'geoip2')) {
|
|
($db, $dbapi) = $self->load_geoip2($geodb_opts);
|
|
$loaded = 'geoip2' if $db;
|
|
}
|
|
|
|
## Geo::IP
|
|
if (!$db && (!$geodb_opts->{module} || $geodb_opts->{module} eq 'geoip')) {
|
|
($db, $dbapi) = $self->load_geoip($geodb_opts);
|
|
$loaded = 'geoip' if $db;
|
|
}
|
|
|
|
## IP::Country::DB_File
|
|
if (!$db && $geodb_opts->{module} && $geodb_opts->{module} eq 'dbfile') {
|
|
# Only try if geodb_module and path to ipcc.db specified
|
|
($db, $dbapi) = $self->load_dbfile($geodb_opts);
|
|
$loaded = 'dbfile' if $db;
|
|
}
|
|
|
|
## IP::Country::Fast
|
|
if (!$db && (!$geodb_opts->{module} || $geodb_opts->{module} eq 'fast')) {
|
|
($db, $dbapi) = $self->load_fast($geodb_opts);
|
|
$loaded = 'fast' if $db;
|
|
}
|
|
|
|
if (!$db) {
|
|
dbg("geodb: No supported database could be loaded");
|
|
die("No supported GeoDB database could be loaded\n");
|
|
}
|
|
|
|
# country can be aliased to city
|
|
if (!$dbapi->{country} && $dbapi->{city}) {
|
|
$dbapi->{country} = $dbapi->{city};
|
|
}
|
|
if (!$dbapi->{country_v6} && $dbapi->{city_v6}) {
|
|
$dbapi->{country_v6} = $dbapi->{city_v6}
|
|
}
|
|
# GeoIP2 asn can be aliased to isp
|
|
if ($loaded eq 'geoip2') {
|
|
if (!$dbapi->{asn} && $dbapi->{isp}) {
|
|
$dbapi->{asn} = $dbapi->{isp};
|
|
}
|
|
if (!$dbapi->{asn_v6} && $dbapi->{isp_v6}) {
|
|
$dbapi->{asn_v6} = $dbapi->{isp_v6}
|
|
}
|
|
}
|
|
|
|
$self->{db} = $db;
|
|
$self->{dbapi} = $dbapi;
|
|
|
|
foreach (@{$self->get_dbinfo()}) {
|
|
dbg("geodb: database info: ".$_);
|
|
}
|
|
#dbg("geodb: apis available: ".join(', ', sort keys %{$self->{dbapi}}));
|
|
|
|
return 1;
|
|
}
|
|
|
|
sub load_geoip2 {
|
|
my ($self, $geodb_opts) = @_;
|
|
my ($db, $dbapi, $ok);
|
|
|
|
# Warn about fatal errors if this module was specifically requested
|
|
my $errwarn = ($geodb_opts->{module}||'') eq 'geoip2';
|
|
|
|
eval {
|
|
require MaxMind::DB::Reader;
|
|
} or do {
|
|
my $err = $@;
|
|
$err =~ s/ at .*//s;
|
|
$err = "geodb: MaxMind::DB::Reader (GeoIP2) module load failed: $err";
|
|
$errwarn ? warn("$err\n") : dbg($err);
|
|
return (undef, undef);
|
|
};
|
|
|
|
my %path;
|
|
foreach my $dbtype (@geoip_types) {
|
|
# skip country if city already loaded
|
|
next if $dbtype eq 'country' && $db->{city};
|
|
# skip asn if isp already loaded
|
|
next if $dbtype eq 'asn' && $db->{isp};
|
|
# skip if not needed
|
|
next if $geodb_opts->{wanted} && !$geodb_opts->{wanted}->{$dbtype};
|
|
# only autosearch if no absolute path given
|
|
if (!defined $geodb_opts->{dbs}->{$dbtype}) {
|
|
# Try some default locations
|
|
PATHS_GEOIP2: foreach my $p (@{$geodb_opts->{search_path}}) {
|
|
foreach my $f (@{$geoip2_default_files{$dbtype}}) {
|
|
if (-f "$p/$f") {
|
|
$path{$dbtype} = "$p/$f";
|
|
dbg("geodb: GeoIP2: search found $dbtype $p/$f");
|
|
last PATHS_GEOIP2;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
if (!-f $geodb_opts->{dbs}->{$dbtype}) {
|
|
dbg("geodb: GeoIP2: $dbtype database requested, but not found: ".
|
|
$geodb_opts->{dbs}->{$dbtype});
|
|
next;
|
|
}
|
|
$path{$dbtype} = $geodb_opts->{dbs}->{$dbtype};
|
|
}
|
|
|
|
if (defined $path{$dbtype}) {
|
|
eval {
|
|
$db->{$dbtype} = MaxMind::DB::Reader->new(
|
|
file => $path{$dbtype},
|
|
);
|
|
die "unknown error" unless $db->{$dbtype};
|
|
1;
|
|
};
|
|
if ($@ || !$db->{$dbtype}) {
|
|
my $err = $@;
|
|
$err =~ s/\s+Trace begun.*//s;
|
|
$err =~ s/ at .*//s;
|
|
dbg("geodb: GeoIP2: $dbtype load failed: $err");
|
|
} else {
|
|
dbg("geodb: GeoIP2: loaded $dbtype from $path{$dbtype}");
|
|
$ok = 1;
|
|
}
|
|
} else {
|
|
my $from = defined $geodb_opts->{dbs}->{$dbtype} ?
|
|
$geodb_opts->{dbs}->{$dbtype} : "default locations";
|
|
dbg("geodb: GeoIP2: $dbtype database not found from $from");
|
|
}
|
|
}
|
|
|
|
if (!$ok) {
|
|
warn("geodb: GeoIP2 requested, but no databases could be loaded\n") if $errwarn;
|
|
return (undef, undef)
|
|
}
|
|
|
|
# dbinfo_DBTYPE()
|
|
$db->{city} and $dbapi->{dbinfo_city} = sub {
|
|
my $m = $_[0]->{db}->{city}->metadata();
|
|
return "GeoIP2 city: ".$m->description()->{en}." / ".localtime($m->build_epoch());
|
|
};
|
|
$db->{country} and $dbapi->{dbinfo_country} = sub {
|
|
my $m = $_[0]->{db}->{country}->metadata();
|
|
return "GeoIP2 country: ".$m->description()->{en}." / ".localtime($m->build_epoch());
|
|
};
|
|
$db->{isp} and $dbapi->{dbinfo_isp} = sub {
|
|
my $m = $_[0]->{db}->{isp}->metadata();
|
|
return "GeoIP2 isp: ".$m->description()->{en}." / ".localtime($m->build_epoch());
|
|
};
|
|
$db->{asn} and $dbapi->{dbinfo_asn} = sub {
|
|
my $m = $_[0]->{db}->{asn}->metadata();
|
|
return "GeoIP2 asn: ".$m->description()->{en}." / ".localtime($m->build_epoch());
|
|
};
|
|
|
|
# city()
|
|
$db->{city} and $dbapi->{city} = $dbapi->{city_v6} = sub {
|
|
my $res = {};
|
|
my $city;
|
|
eval {
|
|
$city = $_[0]->{db}->{city}->record_for_address($_[1]);
|
|
1;
|
|
} or do {
|
|
$@ =~ s/\s+Trace begun.*//s;
|
|
dbg("geodb: GeoIP2 city query failed for $_[1]: $@");
|
|
return $res;
|
|
};
|
|
eval {
|
|
$res->{city_name} = $city->{city}->{names}->{en};
|
|
$res->{country} = $city->{country}->{iso_code};
|
|
$res->{country_name} = $city->{country}->{names}->{en};
|
|
$res->{continent} = $city->{continent}->{code};
|
|
$res->{continent_name} = $city->{continent}->{names}->{en};
|
|
1;
|
|
};
|
|
return $res;
|
|
};
|
|
|
|
# country()
|
|
$db->{country} and $dbapi->{country} = $dbapi->{country_v6} = sub {
|
|
my $res = {};
|
|
my $country;
|
|
eval {
|
|
$country = $_[0]->{db}->{country}->record_for_address($_[1]);
|
|
1;
|
|
} or do {
|
|
$@ =~ s/\s+Trace begun.*//s;
|
|
dbg("geodb: GeoIP2 country query failed for $_[1]: $@");
|
|
return $res;
|
|
};
|
|
eval {
|
|
$res->{country} = $country->{country}->{iso_code};
|
|
$res->{country_name} = $country->{country}->{names}->{en};
|
|
$res->{continent} = $country->{continent}->{code};
|
|
$res->{continent_name} = $country->{continent}->{names}->{en};
|
|
1;
|
|
};
|
|
return $res;
|
|
};
|
|
|
|
# isp()
|
|
$db->{isp} and $dbapi->{isp} = $dbapi->{isp_v6} = sub {
|
|
my $res = {};
|
|
my $isp;
|
|
eval {
|
|
$isp = $_[0]->{db}->{isp}->record_for_address($_[1]);
|
|
1;
|
|
} or do {
|
|
$@ =~ s/\s+Trace begun.*//s;
|
|
dbg("geodb: GeoIP2 isp query failed for $_[1]: $@");
|
|
return $res;
|
|
};
|
|
eval {
|
|
$res->{asn} = $isp->{autonomous_system_number};
|
|
$res->{asn_organization} = $isp->{autonomous_system_organization};
|
|
$res->{isp} = $isp->{isp};
|
|
$res->{organization} = $isp->{organization};
|
|
1;
|
|
};
|
|
return $res;
|
|
};
|
|
|
|
# asn()
|
|
$db->{asn} and $dbapi->{asn} = $dbapi->{asn_v6} = sub {
|
|
my $res = {};
|
|
my $asn;
|
|
eval {
|
|
$asn = $_[0]->{db}->{asn}->record_for_address($_[1]);
|
|
1;
|
|
} or do {
|
|
$@ =~ s/\s+Trace begun.*//s;
|
|
dbg("geodb: GeoIP2 asn query failed for $_[1]: $@");
|
|
return $res;
|
|
};
|
|
eval {
|
|
$res->{asn} = $asn->{autonomous_system_number};
|
|
$res->{asn_organization} = $asn->{autonomous_system_organization};
|
|
1;
|
|
};
|
|
return $res;
|
|
};
|
|
|
|
return ($db, $dbapi);
|
|
}
|
|
|
|
sub load_geoip {
|
|
my ($self, $geodb_opts) = @_;
|
|
my ($db, $dbapi, $ok);
|
|
my ($gic_wanted, $gic_have, $gip_wanted, $gip_have);
|
|
my ($flags, $fix_stderr, $can_ipv6);
|
|
|
|
# Warn about fatal errors if this module was specifically requested
|
|
my $errwarn = ($geodb_opts->{module}||'') eq 'geoip';
|
|
|
|
eval {
|
|
require Geo::IP;
|
|
# need GeoIP C library 1.6.3 and GeoIP perl API 1.4.4 or later to avoid messages leaking - Bug 7153
|
|
$gip_wanted = version->parse('v1.4.4');
|
|
$gip_have = version->parse(Geo::IP->VERSION);
|
|
$gic_wanted = version->parse('v1.6.3');
|
|
eval { $gic_have = version->parse(Geo::IP->lib_version()); }; # might not have lib_version()
|
|
$gic_have = 'none' if !defined $gic_have;
|
|
dbg("geodb: GeoIP: versions: Geo::IP $gip_have, C library $gic_have");
|
|
$flags = 0;
|
|
$fix_stderr = 0;
|
|
if (ref($gic_have) eq 'version') {
|
|
# this code burps an ugly message if it fails, but that's redirected elsewhere
|
|
eval '$flags = Geo::IP::GEOIP_SILENCE' if $gip_wanted >= $gip_have;
|
|
$fix_stderr = $flags && $gic_wanted >= $gic_have;
|
|
}
|
|
$can_ipv6 = Geo::IP->VERSION >= 1.39 && Geo::IP->api eq 'CAPI';
|
|
1;
|
|
} or do {
|
|
my $err = $@;
|
|
$err =~ s/ at .*//s;
|
|
$err = "geodb: Geo::IP module load failed: $err";
|
|
$errwarn ? warn("$err\n") : dbg($err);
|
|
return (undef, undef);
|
|
};
|
|
|
|
my %path;
|
|
foreach my $dbtype (@geoip_types) {
|
|
# skip country if city already loaded
|
|
next if $dbtype eq 'country' && $db->{city};
|
|
# skip asn if isp already loaded
|
|
next if $dbtype eq 'asn' && $db->{isp};
|
|
# skip if not needed
|
|
next if $geodb_opts->{wanted} && !$geodb_opts->{wanted}->{$dbtype};
|
|
# only autosearch if no absolute path given
|
|
if (!defined $geodb_opts->{dbs}->{$dbtype}) {
|
|
# Try some default locations
|
|
PATHS_GEOIP: foreach my $p (@{$geodb_opts->{search_path}}) {
|
|
foreach my $f (@{$geoip_default_files{$dbtype}}) {
|
|
if (-f "$p/$f") {
|
|
$path{$dbtype} = "$p/$f";
|
|
dbg("geodb: GeoIP: search found $dbtype $p/$f");
|
|
if ($can_ipv6 && $f =~ s/\.(dat)$/v6.$1/i) {
|
|
if (-f "$p/$f") {
|
|
$path{$dbtype."_v6"} = "$p/$f";
|
|
dbg("geodb: GeoIP: search found $dbtype $p/$f");
|
|
}
|
|
}
|
|
last PATHS_GEOIP;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
if (!-f $geodb_opts->{dbs}->{$dbtype}) {
|
|
dbg("geodb: GeoIP: $dbtype database requested, but not found: ".
|
|
$geodb_opts->{dbs}->{$dbtype});
|
|
next;
|
|
}
|
|
$path{$dbtype} = $geodb_opts->{dbs}->{$dbtype};
|
|
}
|
|
}
|
|
|
|
if (!$can_ipv6) {
|
|
dbg("geodb: GeoIP: IPv6 support not enabled, versions Geo::IP 1.39, GeoIP C API 1.4.7 required");
|
|
}
|
|
|
|
if ($fix_stderr) {
|
|
open(OLDERR, ">&STDERR");
|
|
open(STDERR, ">/dev/null");
|
|
}
|
|
foreach my $dbtype (@geoip_types) {
|
|
next unless defined $path{$dbtype};
|
|
eval {
|
|
$db->{$dbtype} = Geo::IP->open($path{$dbtype}, Geo::IP->GEOIP_STANDARD | $flags);
|
|
if ($can_ipv6 && defined $path{$dbtype."_v6"}) {
|
|
$db->{$dbtype."_v6"} = Geo::IP->open($path{$dbtype."_v6"}, Geo::IP->GEOIP_STANDARD | $flags);
|
|
}
|
|
};
|
|
if ($@ || !$db->{$dbtype}) {
|
|
my $err = $@;
|
|
$err =~ s/ at .*//s;
|
|
dbg("geodb: GeoIP: database $path{$dbtype} load failed: $err");
|
|
} else {
|
|
dbg("geodb: GeoIP: loaded $dbtype from $path{$dbtype}");
|
|
$ok = 1;
|
|
}
|
|
}
|
|
if ($fix_stderr) {
|
|
open(STDERR, ">&OLDERR");
|
|
close(OLDERR);
|
|
}
|
|
|
|
if (!$ok) {
|
|
warn("geodb: GeoIP requested, but no databases could be loaded\n") if $errwarn;
|
|
return (undef, undef)
|
|
}
|
|
|
|
# dbinfo_DBTYPE()
|
|
$db->{city} and $dbapi->{dbinfo_city} = sub {
|
|
return "Geo::IP IPv4 city: " . ($_[0]->{db}->{city}->database_info || '?')." / IPv6: ".
|
|
($_[0]->{db}->{city_v6} ? $_[0]->{db}->{city_v6}->database_info || '?' : 'no')
|
|
};
|
|
$db->{country} and $dbapi->{dbinfo_country} = sub {
|
|
return "Geo::IP IPv4 country: " . ($_[0]->{db}->{country}->database_info || '?')." / IPv6: ".
|
|
($_[0]->{db}->{country_v6} ? $_[0]->{db}->{country_v6}->database_info || '?' : 'no')
|
|
};
|
|
$db->{isp} and $dbapi->{dbinfo_isp} = sub {
|
|
return "Geo::IP IPv4 isp: " . ($_[0]->{db}->{isp}->database_info || '?')." / IPv6: ".
|
|
($_[0]->{db}->{isp_v6} ? $_[0]->{db}->{isp_v6}->database_info || '?' : 'no')
|
|
};
|
|
$db->{asn} and $dbapi->{dbinfo_asn} = sub {
|
|
return "Geo::IP IPv4 asn: " . ($_[0]->{db}->{asn}->database_info || '?')." / IPv6: ".
|
|
($_[0]->{db}->{asn_v6} ? $_[0]->{db}->{asn_v6}->database_info || '?' : 'no')
|
|
};
|
|
|
|
# city()
|
|
$db->{city} and $dbapi->{city} = sub {
|
|
my $res = {};
|
|
my $city;
|
|
if ($_[1] =~ IS_IPV4_ADDRESS) {
|
|
$city = $_[0]->{db}->{city}->record_by_addr($_[1]);
|
|
} elsif ($_[0]->{db}->{city_v6}) {
|
|
$city = $_[0]->{db}->{city_v6}->record_by_addr_v6($_[1]);
|
|
}
|
|
if (!defined $city) {
|
|
dbg("geodb: GeoIP city query failed for $_[1]");
|
|
return $res;
|
|
}
|
|
$res->{city_name} = $city->city;
|
|
$res->{country} = $city->country_code;
|
|
$res->{country_name} = $city->country_name;
|
|
$res->{continent} = $city->continent_code;
|
|
return $res;
|
|
};
|
|
$dbapi->{city_v6} = $dbapi->{city} if $db->{city_v6};
|
|
|
|
# country()
|
|
$db->{country} and $dbapi->{country} = sub {
|
|
my $res = {};
|
|
my $country;
|
|
eval {
|
|
if ($_[1] =~ IS_IPV4_ADDRESS) {
|
|
$country = $_[0]->{db}->{country}->country_code_by_addr($_[1]);
|
|
} elsif ($_[0]->{db}->{country_v6}) {
|
|
$country = $_[0]->{db}->{country_v6}->country_code_by_addr_v6($_[1]);
|
|
}
|
|
1;
|
|
};
|
|
if (!defined $country) {
|
|
dbg("geodb: GeoIP country query failed for $_[1]");
|
|
return $res;
|
|
};
|
|
$res->{country} = $country || 'XX';
|
|
$res->{continent} = $country_to_continent{$country} || 'XX';
|
|
return $res;
|
|
};
|
|
$dbapi->{country_v6} = $dbapi->{country} if $db->{country_v6};
|
|
|
|
# isp()
|
|
$db->{isp} and $dbapi->{isp} = sub {
|
|
my $res = {};
|
|
my $isp;
|
|
eval {
|
|
if ($_[1] =~ IS_IPV4_ADDRESS) {
|
|
$isp = $_[0]->{db}->{isp}->isp_by_addr($_[1]);
|
|
} else {
|
|
# TODO?
|
|
return $res;
|
|
}
|
|
1;
|
|
};
|
|
if (!defined $isp) {
|
|
dbg("geodb: GeoIP isp query failed for $_[1]");
|
|
return $res;
|
|
};
|
|
$res->{isp} = $isp;
|
|
return $res;
|
|
};
|
|
|
|
# asn()
|
|
$db->{asn} and $dbapi->{asn} = sub {
|
|
my $res = {};
|
|
my $asn;
|
|
eval {
|
|
if ($_[1] =~ IS_IPV4_ADDRESS) {
|
|
$asn = $_[0]->{db}->{asn}->isp_by_addr($_[1]);
|
|
} else {
|
|
# TODO?
|
|
return $res;
|
|
}
|
|
1;
|
|
};
|
|
if (!defined $asn || $asn !~ /^((?:AS)?\d+)(?:\s+(.+))?/) {
|
|
dbg("geodb: GeoIP asn query failed for $_[1]");
|
|
return $res;
|
|
};
|
|
$res->{asn} = $1;
|
|
$res->{asn_organization} = $2 if defined $2;
|
|
return $res;
|
|
};
|
|
|
|
return ($db, $dbapi);
|
|
}
|
|
|
|
sub load_dbfile {
|
|
my ($self, $geodb_opts) = @_;
|
|
my ($db, $dbapi);
|
|
|
|
# Warn about fatal errors if this module was specifically requested
|
|
my $errwarn = ($geodb_opts->{module}||'') eq 'dbfile';
|
|
|
|
if (!defined $geodb_opts->{dbs}->{country}) {
|
|
my $err = "geodb: IP::Country::DB_File requires geodb_options country:/path/to/ipcc.db";
|
|
$errwarn ? warn("$err\n") : dbg($err);
|
|
return (undef, undef);
|
|
}
|
|
|
|
if (!-f $geodb_opts->{dbs}->{country}) {
|
|
my $err = "geodb: IP::Country::DB_File database not found: ".$geodb_opts->{dbs}->{country};
|
|
$errwarn ? warn("$err\n") : dbg($err);
|
|
return (undef, undef);
|
|
}
|
|
|
|
eval {
|
|
require IP::Country::DB_File;
|
|
$db->{country} = IP::Country::DB_File->new($geodb_opts->{dbs}->{country});
|
|
1;
|
|
};
|
|
if ($@ || !$db->{country}) {
|
|
my $err = $@;
|
|
$err =~ s/ at .*//s;
|
|
$err = "geodb: IP::Country::DB_File country load failed: $err";
|
|
$errwarn ? warn("$err\n") : dbg($err);
|
|
return (undef, undef);
|
|
} else {
|
|
dbg("geodb: IP::Country::DB_File loaded country from ".$geodb_opts->{dbs}->{country});
|
|
}
|
|
|
|
# dbinfo_DBTYPE()
|
|
$db->{country} and $dbapi->{dbinfo_country} = sub {
|
|
return "IP::Country::DB_File country: ".localtime($_[0]->{db}->{country}->db_time());
|
|
};
|
|
|
|
# country();
|
|
$db->{country} and $dbapi->{country} = $dbapi->{country_v6} = sub {
|
|
my $res = {};
|
|
my $country;
|
|
if ($_[1] =~ IS_IPV4_ADDRESS) {
|
|
$country = $_[0]->{db}->{country}->inet_atocc($_[1]);
|
|
} else {
|
|
$country = $_[0]->{db}->{country}->inet6_atocc($_[1]);
|
|
}
|
|
if (!defined $country) {
|
|
dbg("geodb: IP::Country::DB_File country query failed for $_[1]");
|
|
return $res;
|
|
};
|
|
$res->{country} = $country || 'XX';
|
|
$res->{continent} = $country_to_continent{$country} || 'XX';
|
|
return $res;
|
|
};
|
|
|
|
return ($db, $dbapi);
|
|
}
|
|
|
|
sub load_fast {
|
|
my ($self, $geodb_opts) = @_;
|
|
my ($db, $dbapi);
|
|
|
|
# Warn about fatal errors if this module was specifically requested
|
|
my $errwarn = ($geodb_opts->{module}||'') eq 'fast';
|
|
|
|
eval {
|
|
require IP::Country::Fast;
|
|
$db->{country} = IP::Country::Fast->new();
|
|
1;
|
|
};
|
|
if ($@ || !$db->{country}) {
|
|
my $err = $@;
|
|
$err =~ s/ at .*//s;
|
|
$err = "geodb: IP::Country::Fast load failed: $err";
|
|
$errwarn ? warn("$err\n") : dbg($err);
|
|
return (undef, undef);
|
|
}
|
|
|
|
# dbinfo_DBTYPE()
|
|
$db->{country} and $dbapi->{dbinfo_country} = sub {
|
|
return "IP::Country::Fast country: ".localtime($_[0]->{db}->{country}->db_time());
|
|
};
|
|
|
|
# country();
|
|
$db->{country} and $dbapi->{country} = sub {
|
|
my $res = {};
|
|
my $country;
|
|
if ($_[1] =~ IS_IPV4_ADDRESS) {
|
|
$country = $_[0]->{db}->{country}->inet_atocc($_[1]);
|
|
} else {
|
|
return $res;
|
|
}
|
|
if (!defined $country) {
|
|
dbg("geodb: IP::Country::Fast country query failed for $_[1]");
|
|
return $res;
|
|
};
|
|
$res->{country} = $country || 'XX';
|
|
$res->{continent} = $country_to_continent{$country} || 'XX';
|
|
return $res;
|
|
};
|
|
|
|
return ($db, $dbapi);
|
|
}
|
|
|
|
# return array, infoline per database type
|
|
sub get_dbinfo {
|
|
my ($self, $db) = @_;
|
|
|
|
my @lines;
|
|
foreach (@geoip_types) {
|
|
if (exists $self->{dbapi}->{"dbinfo_".$_}) {
|
|
push @lines,
|
|
$self->{dbapi}->{"dbinfo_".$_}->($self) || "$_ failed";
|
|
}
|
|
}
|
|
|
|
return \@lines;
|
|
}
|
|
|
|
sub get_country {
|
|
my ($self, $ip) = @_;
|
|
|
|
return undef if !defined $ip || $ip !~ /\S/; ## no critic (ProhibitExplicitReturnUndef)
|
|
|
|
if ($ip =~ IS_IP_PRIVATE) {
|
|
return '**';
|
|
}
|
|
|
|
if ($ip !~ IS_IP_ADDRESS) {
|
|
$ip = name_to_ip($ip);
|
|
return 'XX' if !defined $ip;
|
|
}
|
|
|
|
if ($self->{dbapi}->{city}) {
|
|
return $self->_get('city',$ip)->{country} || 'XX';
|
|
} elsif ($self->{dbapi}->{country}) {
|
|
return $self->_get('country',$ip)->{country} || 'XX';
|
|
} else {
|
|
return undef; ## no critic (ProhibitExplicitReturnUndef)
|
|
}
|
|
}
|
|
|
|
sub get_continent {
|
|
my ($self, $ip) = @_;
|
|
|
|
return undef if !defined $ip || $ip !~ /\S/; ## no critic (ProhibitExplicitReturnUndef)
|
|
|
|
# If it's already CC, use our own lookup table..
|
|
if (length($ip) == 2) {
|
|
return $country_to_continent{uc($ip)} || 'XX';
|
|
}
|
|
|
|
if ($self->{dbapi}->{city}) {
|
|
return $self->_get('city',$ip)->{continent} || 'XX';
|
|
} elsif ($self->{dbapi}->{country}) {
|
|
return $self->_get('country',$ip)->{continent} || 'XX';
|
|
} else {
|
|
return undef; ## no critic (ProhibitExplicitReturnUndef)
|
|
}
|
|
}
|
|
|
|
sub get_isp {
|
|
my ($self, $ip) = @_;
|
|
|
|
return undef if !defined $ip || $ip !~ /\S/; ## no critic (ProhibitExplicitReturnUndef)
|
|
|
|
if ($self->{dbapi}->{isp}) {
|
|
return $self->_get('isp',$ip)->{isp};
|
|
} else {
|
|
return undef; ## no critic (ProhibitExplicitReturnUndef)
|
|
}
|
|
}
|
|
|
|
sub get_isp_org {
|
|
my ($self, $ip) = @_;
|
|
|
|
return undef if !defined $ip || $ip !~ /\S/; ## no critic (ProhibitExplicitReturnUndef)
|
|
|
|
if ($self->{dbapi}->{isp}) {
|
|
return $self->_get('isp',$ip)->{organization};
|
|
} else {
|
|
return undef; ## no critic (ProhibitExplicitReturnUndef)
|
|
}
|
|
}
|
|
|
|
sub get_asn {
|
|
my ($self, $ip) = @_;
|
|
|
|
return undef if !defined $ip || $ip !~ /\S/; ## no critic (ProhibitExplicitReturnUndef)
|
|
|
|
if ($self->{dbapi}->{asn}) {
|
|
return $self->_get('asn',$ip)->{asn};
|
|
} elsif ($self->{dbapi}->{isp}) {
|
|
return $self->_get('isp',$ip)->{asn};
|
|
} else {
|
|
return undef; ## no critic (ProhibitExplicitReturnUndef)
|
|
}
|
|
}
|
|
|
|
sub get_asn_org {
|
|
my ($self, $ip) = @_;
|
|
|
|
return undef if !defined $ip || $ip !~ /\S/; ## no critic (ProhibitExplicitReturnUndef)
|
|
|
|
if ($self->{dbapi}->{asn}) {
|
|
return $self->_get('asn',$ip)->{asn_organization};
|
|
} elsif ($self->{dbapi}->{isp}) {
|
|
return $self->_get('isp',$ip)->{asn_organization};
|
|
} else {
|
|
return undef; ## no critic (ProhibitExplicitReturnUndef)
|
|
}
|
|
}
|
|
|
|
sub get_all {
|
|
my ($self, $ip) = @_;
|
|
|
|
return undef if !defined $ip || $ip !~ /\S/; ## no critic (ProhibitExplicitReturnUndef)
|
|
|
|
my $all = {};
|
|
|
|
if ($ip =~ IS_IP_PRIVATE) {
|
|
return { 'country' => '**' };
|
|
}
|
|
|
|
if ($ip !~ IS_IP_ADDRESS) {
|
|
$ip = name_to_ip($ip);
|
|
if (!defined $ip) {
|
|
return { 'country' => 'XX' };
|
|
}
|
|
}
|
|
|
|
if ($self->{dbapi}->{city}) {
|
|
my $res = $self->_get('city',$ip);
|
|
$all->{$_} = $res->{$_} foreach (keys %$res);
|
|
} elsif ($self->{dbapi}->{country}) {
|
|
my $res = $self->_get('country',$ip);
|
|
$all->{$_} = $res->{$_} foreach (keys %$res);
|
|
}
|
|
|
|
if ($self->{dbapi}->{isp}) {
|
|
my $res = $self->_get('isp',$ip);
|
|
$all->{$_} = $res->{$_} foreach (keys %$res);
|
|
}
|
|
|
|
if ($self->{dbapi}->{asn}) {
|
|
my $res = $self->_get('asn',$ip);
|
|
$all->{$_} = $res->{$_} foreach (keys %$res);
|
|
}
|
|
|
|
return $all;
|
|
}
|
|
|
|
sub can {
|
|
my ($self, $check) = @_;
|
|
|
|
return defined $self->{dbapi}->{$check};
|
|
}
|
|
|
|
# TODO: use SA internal dns synchronously?
|
|
# This shouldn't be called much, as plugins
|
|
# should do their own resolving if needed
|
|
sub name_to_ip {
|
|
my $name = shift;
|
|
if (my $ip = inet_aton($name)) {
|
|
$ip = inet_ntoa($ip);
|
|
dbg("geodb: resolved internally $name: $ip");
|
|
return $ip;
|
|
}
|
|
dbg("geodb: failed to internally resolve $name");
|
|
return undef; ## no critic (ProhibitExplicitReturnUndef)
|
|
}
|
|
|
|
sub _get {
|
|
my ($self, $type, $ip) = @_;
|
|
|
|
# reset cache at 100 ips
|
|
if (scalar keys %{$self->{cache}} >= 100) {
|
|
$self->{cache} = ();
|
|
}
|
|
|
|
if (!exists $self->{cache}{$ip}{$type}) {
|
|
if ($self->{dbapi}->{$type}) {
|
|
$self->{cache}{$ip}{$type} = $self->{dbapi}->{$type}->($self,$ip);
|
|
} else {
|
|
return undef; ## no critic (ProhibitExplicitReturnUndef)
|
|
}
|
|
}
|
|
|
|
return $self->{cache}{$ip}{$type};
|
|
}
|
|
|
|
1;
|