File Coverage

blib/lib/IP/Country/DB_File/Builder.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package IP::Country::DB_File::Builder;
2             $IP::Country::DB_File::Builder::VERSION = '3.03';
3 1     1   4519 use strict;
  1         2  
  1         37  
4 1     1   5 use warnings;
  1         2  
  1         31  
5              
6             # ABSTRACT: Build an IP address to country code database
7              
8 1     1   443 use DB_File ();
  0            
  0            
9             use Fcntl ();
10             use Math::Int64 qw(
11             int64 int64_to_net net_to_int64
12             :native_if_available
13             );
14             use Net::FTP ();
15             use Socket 1.94 ();
16              
17             # Regional Internet Registries
18             my @rirs = (
19             { name=>'arin', server=>'ftp.arin.net' },
20             { name=>'ripencc', server=>'ftp.ripe.net' },
21             { name=>'afrinic', server=>'ftp.afrinic.net' },
22             { name=>'apnic', server=>'ftp.apnic.net' },
23             { name=>'lacnic', server=>'ftp.lacnic.net' },
24             );
25              
26             # Constants
27             sub _EXCLUDE_IPV4 { 1 }
28             sub _EXCLUDE_IPV6 { 2 }
29              
30             # IPv6 support is broken in some Socket versions with older Perls.
31             # (RT #98248)
32             sub _ipv6_socket_broken {
33             return $^V < 5.14 && $Socket::VERSION >= 2.010;
34             }
35              
36             sub _ipv6_supported {
37             my ($err, $result) = Socket::getaddrinfo('::1', undef, {
38             flags => Socket::AI_NUMERICHOST,
39             family => Socket::AF_INET6,
40             socktype => Socket::SOCK_STREAM,
41             });
42              
43             return !$err && $result ? 1 : 0;
44             }
45              
46             sub new {
47             my ($class, $db_file) = @_;
48             $db_file = 'ipcc.db' unless defined($db_file);
49              
50             my $this = {
51             num_ranges_v4 => 0,
52             num_ranges_v6 => 0,
53             num_addresses_v4 => 0,
54             };
55              
56             my %db;
57             my $flags = Fcntl::O_RDWR|Fcntl::O_CREAT|Fcntl::O_TRUNC;
58             $this->{db} = tie(%db, 'DB_File', $db_file, $flags, 0666,
59             $DB_File::DB_BTREE)
60             or die("Can't open database $db_file: $!");
61              
62             return bless($this, $class);
63             }
64              
65             # Accessors
66             sub num_ranges_v4 { $_[0]->{num_ranges_v4} }
67             sub num_ranges_v6 { $_[0]->{num_ranges_v6} }
68             sub num_addresses_v4 { $_[0]->{num_addresses_v4} }
69              
70             sub _store_ip_range {
71             my ($this, $type, $start, $end, $cc) = @_;
72              
73             my ($key, $data);
74              
75             if ($type eq 'ipv4') {
76             $key = pack('aN', '4', $end - 1);
77             $data = pack('Na2', $start, $cc);
78              
79             $this->{num_ranges_v4} += 1;
80             $this->{num_addresses_v4} += $end - $start;
81             }
82             elsif ($type eq 'ipv6') {
83             $key = '6' . int64_to_net($end - 1);
84             $data = pack('a8a2', int64_to_net($start), $cc);
85              
86             $this->{num_ranges_v6} += 1;
87             }
88              
89             $this->{db}->put($key, $data) >= 0 or die("dbput: $!");
90             }
91              
92             sub _store_private_networks {
93             my ($this, $flags) = @_;
94              
95             if (!($flags & _EXCLUDE_IPV4)) {
96             # 10.0.0.0
97             $this->_store_ip_range('ipv4', 0x0a000000, 0x0b000000, '**');
98             # 172.16.0.0
99             $this->_store_ip_range('ipv4', 0xac100000, 0xac200000, '**');
100             # 192.168.0.0
101             $this->_store_ip_range('ipv4', 0xc0a80000, 0xc0a90000, '**');
102             }
103              
104             if (!($flags & _EXCLUDE_IPV6)) {
105             # fc00::/7
106             $this->_store_ip_range(
107             'ipv6',
108             int64(0xfc) << 56, int64(0xfe) << 56,
109             '**',
110             );
111             }
112             }
113              
114             sub _import_file {
115             my ($this, $file, $flags) = @_;
116              
117             my $seen_header;
118             my (@ranges_v4, @ranges_v6);
119              
120             while (my $line = readline($file)) {
121             next if $line =~ /^#/ or $line !~ /\S/;
122              
123             if (!$seen_header) {
124             # Ignore first line.
125             $seen_header = 1;
126             next;
127             }
128              
129             my ($registry, $cc, $type, $start, $value, $date, $status) =
130             split(/\|/, $line);
131              
132             next if $start eq '*'; # Summary lines.
133             next if $cc eq '';
134              
135             $cc = uc($cc);
136             die("Invalid country code '$cc'")
137             if $cc !~ /^[A-Z]{2}\z/;
138              
139             # TODO (paranoid): validate $start and $value
140              
141             if ($type eq 'ipv4') {
142             next if $flags & _EXCLUDE_IPV4;
143              
144             my $ip_num = unpack('N', pack('C4', split(/\./, $start)));
145             my $size = $value;
146              
147             push(@ranges_v4, [ $ip_num, $size, $cc ]);
148             }
149             elsif ($type eq 'ipv6') {
150             next if $flags & _EXCLUDE_IPV6;
151              
152             die("IPv6 range too large: $value")
153             if $value > 64;
154              
155             my ($err, $result) = Socket::getaddrinfo($start, undef, {
156             flags => Socket::AI_NUMERICHOST,
157             family => Socket::AF_INET6,
158             socktype => Socket::SOCK_STREAM,
159             });
160             die($err) if $err;
161             my (undef, $addr) = Socket::unpack_sockaddr_in6($result->{addr});
162              
163             my $ip_num = net_to_int64(substr($addr, 0, 8));
164             my $size = int64(1) << (64 - $value);
165              
166             push(@ranges_v6, [ $ip_num, $size, $cc ]);
167             }
168             else {
169             next;
170             }
171             }
172              
173             my $count = 0;
174             $count += $this->_store_ip_ranges('ipv4', \@ranges_v4);
175             $count += $this->_store_ip_ranges('ipv6', \@ranges_v6);
176              
177             return $count;
178             }
179              
180             sub _store_ip_ranges {
181             my ($this, $type, $ranges) = @_;
182              
183             my @sorted_ranges = sort { $a->[0] <=> $b->[0] } @$ranges;
184              
185             my $count = 0;
186             my $prev_cc = '';
187             my ($prev_start, $prev_end);
188              
189             if ($type eq 'ipv4') {
190             $prev_start = 0;
191             $prev_end = 0;
192             }
193             elsif ($type eq 'ipv6') {
194             $prev_start = int64(0);
195             $prev_end = int64(0);
196             }
197              
198             for my $range (@sorted_ranges) {
199             my ($ip_num, $size, $cc) = @$range;
200              
201             if ($ip_num == $prev_end && $prev_cc eq $cc) {
202             # Concat ranges of same country
203             $prev_end += $size;
204             }
205             else {
206             $this->_store_ip_range($type, $prev_start, $prev_end, $prev_cc)
207             if $prev_cc;
208              
209             $prev_start = $ip_num;
210             $prev_end = $ip_num + $size;
211             $prev_cc = $cc;
212             ++$count;
213             }
214             }
215              
216             $this->_store_ip_range($type, $prev_start, $prev_end, $prev_cc)
217             if $prev_cc;
218              
219             return $count;
220             }
221              
222             sub _sync {
223             my $this = shift;
224              
225             $this->{db}->sync() >= 0 or die("dbsync: $!");
226             }
227              
228             sub build {
229             my ($this, $dir, $flags) = @_;
230             $dir = '.' if !defined($dir);
231             $flags = 0 if !defined($flags);
232              
233             if (!($flags & _EXCLUDE_IPV6) && !_ipv6_supported()) {
234             warn("IPv6 support disabled. It doesn't seem to be supported on"
235             . " your system.");
236             warn("This is probably because getaddrinfo is broken in Perl $^V"
237             . " with Socket $Socket::VERSION.")
238             if _ipv6_socket_broken();
239             $flags |= _EXCLUDE_IPV6;
240             }
241              
242             for my $rir (@rirs) {
243             my $file;
244             my $filename = "$dir/delegated-$rir->{name}";
245             CORE::open($file, '<', $filename)
246             or die("Can't open $filename: $!, " .
247             "maybe you have to fetch files first");
248              
249             eval {
250             $this->_import_file($file, $flags);
251             };
252              
253             my $error = $@;
254             close($file);
255             die("$filename: $error") if $error;
256             }
257              
258             $this->_store_private_networks($flags);
259              
260             $this->_sync();
261             }
262              
263             sub fetch_files {
264             my ($class, $dir, $verbose) = @_;
265             $dir = '.' unless defined($dir);
266              
267             for my $rir (@rirs) {
268             my $server = $rir->{server};
269             my $name = $rir->{name};
270             my $ftp_dir = "/pub/stats/$name";
271             my $filename = "delegated-$name-extended-latest";
272              
273             print("Fetching ftp://$server$ftp_dir/$filename\n") if $verbose;
274              
275             my $ftp = Net::FTP->new($server)
276             or die("Can't connect to FTP server $server: $@");
277             $ftp->login('anonymous', '-anonymous@')
278             or die("Can't login to FTP server $server: " . $ftp->message());
279             $ftp->cwd($ftp_dir)
280             or die("Can't find directory $ftp_dir on FTP server $server: " .
281             $ftp->message());
282             $ftp->get($filename, "$dir/delegated-$name")
283             or die("Get $filename from FTP server $server failed: " .
284             $ftp->message());
285             $ftp->quit();
286             }
287             }
288              
289             sub remove_files {
290             my ($class, $dir) = @_;
291             $dir = '.' unless defined($dir);
292              
293             for my $rir (@rirs) {
294             my $name = $rir->{name};
295             unlink("$dir/delegated-$name");
296             }
297             }
298              
299             1;
300              
301             __END__