1#!/usr/local/bin/perl
2#-----------------------------------------------------------------------------
3# GeoIp2 Maxmind AWStats plugin
4# This plugin allow you to get country report with countries detected
5# from a Geographical database (GeoIP2 internal database) instead of domain
6# hostname suffix.
7# Need the country database from Maxmind (free).
8#-----------------------------------------------------------------------------
9# Perl Required Module: GeoIP2::Database::Reader
10#-----------------------------------------------------------------------------
11
12
13# <-----
14# ENTER HERE THE USE COMMAND FOR ALL REQUIRED PERL MODULES
15use vars qw/ $type /;
16$type='geoip2_country';
17if (!eval ('require "GeoIP2/Database/Reader.pm";')) {
18	$error=$@;
19    $ret=($error)?"Error:\n$error":"";
20    $ret.="Error: Need Perl module GeoIP2::Database::Reader";
21    return $ret;
22}
23# GeoIP2 Perl API doesn't have a ByName lookup so we need to do the resolution ourselves
24if (!eval ('require "Socket.pm";')) {
25	$error=$@;
26    $ret=($error)?"Error:\n$error":"";
27    $ret.="Error: Need Perl module Socket";
28    return $ret;
29}
30# ----->
31#use strict;
32no strict "refs";
33
34
35
36#-----------------------------------------------------------------------------
37# PLUGIN VARIABLES
38#-----------------------------------------------------------------------------
39# <-----
40# ENTER HERE THE MINIMUM AWSTATS VERSION REQUIRED BY YOUR PLUGIN
41# AND THE NAME OF ALL FUNCTIONS THE PLUGIN MANAGE.
42my $PluginNeedAWStatsVersion="5.4";
43my $PluginHooksFunctions="GetCountryCodeByAddr GetCountryCodeByName ShowInfoHost";
44my $PluginName = "geoip2_country";
45my $LoadedOverride=0;
46my $OverrideFile="";
47my %TmpDomainLookup;
48# ----->
49
50# <-----
51# IF YOUR PLUGIN NEED GLOBAL VARIABLES, THEY MUST BE DECLARED HERE.
52use vars qw/
53$reader
54/;
55use Data::Validate::IP 0.25 qw( is_public_ip );
56# ----->
57
58
59#-----------------------------------------------------------------------------
60# PLUGIN FUNCTION: Init_pluginname
61#-----------------------------------------------------------------------------
62sub Init_geoip2_country {
63	my $InitParams=shift;
64	my $checkversion=&Check_Plugin_Version($PluginNeedAWStatsVersion);
65
66	# <-----
67	# ENTER HERE CODE TO DO INIT PLUGIN ACTIONS
68	debug(" Plugin $PluginName: InitParams=$InitParams",1);
69    my ($datafile,$override)=split(/\+/,$InitParams,2);
70   	if (! $datafile) { $datafile="GeoLite2-Country.mmdb"; }
71    else { $datafile =~ s/%20/ /g; }
72	if ($override){$OverrideFile=$override;}
73	%TmpDomainLookup=();
74	debug(" Plugin $PluginName: GeoIP2 try to initialize override=$override datafile=$datafile",1);
75	$reader = GeoIP2::Database::Reader->new(
76        file    => $datafile,
77        locales => [ 'en', 'de', ]
78    );
79
80	# Fails on some GeoIP version
81	# debug(" Plugin $PluginName: GeoIP initialized database_info=".$reader->database_info());
82	if ($reader) { debug(" Plugin $PluginName: GeoIP2 plugin and reader object initialized",1); }
83	else { return "Error: Failed to create reader object for datafile=".$datafile; }
84	# ----->
85
86	return ($checkversion?$checkversion:"$PluginHooksFunctions");
87}
88
89
90#-----------------------------------------------------------------------------
91# PLUGIN FUNCTION: GetCountryCodeByAddr_pluginname
92# UNIQUE: YES (Only one plugin using this function can be loaded)
93# GetCountryCodeByAddr is called to translate an ip into a country code in lower case.
94#-----------------------------------------------------------------------------
95sub GetCountryCodeByAddr_geoip2_country {
96	my $param = shift;
97	if (! $param) { return ''; }
98	my $res = Lookup_geoip2_country($param);
99	return ($res) ? lc($res) : 'unknown';
100}
101
102
103#-----------------------------------------------------------------------------
104# PLUGIN FUNCTION: GetCountryCodeByName_pluginname
105# UNIQUE: YES (Only one plugin using this function can be loaded)
106# GetCountryCodeByName is called to translate a host name into a country code in lower case.
107#-----------------------------------------------------------------------------
108sub GetCountryCodeByName_geoip2_country {
109	return GetCountryCodeByAddr_geoip2_country(@_);
110}
111
112
113#-----------------------------------------------------------------------------
114# PLUGIN FUNCTION: ShowInfoHost_pluginname
115# UNIQUE: NO (Several plugins using this function can be loaded)
116# Function called to add additionnal columns to the Hosts report.
117# This function is called when building rows of the report (One call for each
118# row). So it allows you to add a column in report, for example with code :
119#   print "<TD>This is a new cell for $param</TD>";
120# Parameters: Host name or ip
121#-----------------------------------------------------------------------------
122sub ShowInfoHost_geoip2_country {
123    my $param="$_[0]";
124	# <-----
125	if ($param eq '__title__') {
126    	my $NewLinkParams=${QueryString};
127    	$NewLinkParams =~ s/(^|&)update(=\w*|$)//i;
128    	$NewLinkParams =~ s/(^|&)output(=\w*|$)//i;
129    	$NewLinkParams =~ s/(^|&)staticlinks(=\w*|$)//i;
130    	$NewLinkParams =~ s/(^|&)framename=[^&]*//i;
131    	my $NewLinkTarget='';
132    	if ($DetailedReportsOnNewWindows) { $NewLinkTarget=" target=\"awstatsbis\""; }
133    	if (($FrameName eq 'mainleft' || $FrameName eq 'mainright') && $DetailedReportsOnNewWindows < 2) {
134    		$NewLinkParams.="&framename=mainright";
135    		$NewLinkTarget=" target=\"mainright\"";
136    	}
137    	$NewLinkParams =~ tr/&/&/s; $NewLinkParams =~ s/^&//; $NewLinkParams =~ s/&$//;
138    	if ($NewLinkParams) { $NewLinkParams="${NewLinkParams}&"; }
139
140		print "<th width=\"80\">";
141        print "<a href=\"#countries\">GeoIP2<br />Country</a>";
142        print "</th>";
143	}
144	elsif ($param) {
145		my $res = Lookup_geoip2_country($param);
146		if ($res) {
147				$res = lc($res);
148				print $DomainsHashIDLib{$res}?$DomainsHashIDLib{$res}:"<span style=\"color: #$color_other\">$Message[0]</span>";
149		}
150		else { print "<span style=\"color: #$color_other\">$Message[0]</span>"; }
151		print "</td>";
152	}
153	else {
154		print "<td>&nbsp;</td>";
155	}
156	return 1;
157	# ----->
158}
159
160#-----------------------------------------------------------------------------
161# PLUGIN FUNCTION: LoadOverrideFile
162# Attempts to load a comma delimited file that will override the GeoIP database
163# Useful for Intranet records
164# CSV format: IP,2-char Country code
165#-----------------------------------------------------------------------------
166sub LoadOverrideFile_geoip2_country{
167	my $filetoload="";
168	if ($OverrideFile){
169		if (!open(GEOIPFILE, $OverrideFile)){
170			debug("Plugin $PluginName: Unable to open override file: $OverrideFile");
171			$LoadedOverride = 1;
172			return;
173		}
174	}else{
175		my $conf = (exists(&Get_Config_Name) ? Get_Config_Name() : $SiteConfig);
176		if ($conf && open(GEOIPFILE,"$DirData/$PluginName.$conf.txt"))	{ $filetoload="$DirData/$PluginName.$conf.txt"; }
177		elsif (open(GEOIPFILE,"$DirData/$PluginName.txt"))	{ $filetoload="$DirData/$PluginName.txt"; }
178		else { debug("No override file \"$DirData/$PluginName.txt\": $!"); }
179	}
180	if ($filetoload)
181	{
182		# This is the fastest way to load with regexp that I know
183		while (<GEOIPFILE>){
184			chomp $_;
185			s/\r//;
186			my @record = split(",", $_);
187			# replace quotes if they were used in the file
188			foreach (@record){ $_ =~ s/"//g; }
189			# store in hash
190			$TmpDomainLookup{$record[0]} = $record[1];
191		}
192		close GEOIPFILE;
193        debug(" Plugin $PluginName: Overload file loaded: ".(scalar keys %TmpDomainLookup)." entries found.");
194	}
195	$LoadedOverride = 1;
196	return;
197}
198
199#-----------------------------------------------------------------------------
200# PLUGIN FUNCTION: Lookup
201# Looks up the input parameter (either ip address or dns name) and returns its
202# associated country code; or undefined if not available.
203# GEOIP entry
204#-----------------------------------------------------------------------------
205sub Lookup_geoip2_country {
206	$param = shift;
207	if (!$LoadedOverride) { &LoadOverrideFile_geoip2_country(); }
208	if ($Debug) { debug("  Plugin $PluginName: Lookup_geoip2_country for $param",5); }
209	if ($reader && !exists($TmpDomainLookup{$param})) {
210		$TmpDomainLookup{$param} = undef; # negative entry to avoid repeated lookups
211		# Resolve the parameter (either a name or an ip address) to a list of network addresses
212		my ($err, @result) = Socket::getaddrinfo($param, undef, { protocol => Socket::IPPROTO_TCP, socktype => Socket::SOCK_STREAM });
213		for (@result) {
214			# Convert the network address to human-readable form
215			my ($err, $address, $servicename) = Socket::getnameinfo($_->{addr}, Socket::NI_NUMERICHOST, Socket::NIx_NOSERV);
216			next if ($err || !is_public_ip($address));
217
218			if ($Debug && $param ne $address) { debug("  Plugin $PluginName: Lookup_geoip2_country $param resolved to $address",5); }
219			eval {
220				my $record = $reader->country(ip => $address);
221				$TmpDomainLookup{$param} = $record->country()->iso_code();
222				last;
223			}
224		}
225	}
226	my $res = $TmpDomainLookup{$param};
227	if ($Debug) { debug("  Plugin $PluginName: Lookup_geoip2_country for $param: [$res]",5); }
228	return $res;
229}
230
2311;	# Do not remove this line
232