File Coverage

blib/lib/Geo/SypexGeo.pm
Criterion Covered Total %
statement 191 218 87.6
branch 56 102 54.9
condition 8 18 44.4
subroutine 24 24 100.0
pod 0 11 0.0
total 279 373 74.8


line stmt bran cond sub pod time code
1             package Geo::SypexGeo;
2              
3             our $VERSION = '0.8';
4              
5 2     2   28203 use strict;
  2         3  
  2         62  
6 2     2   8 use warnings;
  2         3  
  2         54  
7 2     2   14 use utf8;
  2         2  
  2         10  
8 2     2   70 use v5.10;
  2         5  
9              
10 2     2   7 use Carp qw( croak );
  2         2  
  2         136  
11 2     2   1185 use Encode;
  2         16850  
  2         150  
12 2     2   1141 use Socket;
  2         7265  
  2         945  
13 2     2   1031 use POSIX;
  2         10900  
  2         12  
14 2     2   5650 use Text::Trim;
  2         1018  
  2         147  
15 2     2   817 use Geo::SypexGeo::Info;
  2         4  
  2         82  
16              
17 2         10 use fields qw(
18             db_file b_idx_str m_idx_str range b_idx_len m_idx_len db_items id_len
19             block_len max_region max_city db_begin regions_begin cities_begin
20             max_country country_size pack
21 2     2   1177 );
  2         2791  
22              
23             use constant {
24 2         1587 HEADER_LENGTH => 40,
25 2     2   250 };
  2         3  
26              
27             my @COUNTRY_ISO_MAP = (
28             '', 'ap', 'eu', 'ad', 'ae', 'af', 'ag', 'ai', 'al', 'am', 'cw', 'ao',
29             'aq', 'ar', 'as', 'at', 'au', 'aw', 'az', 'ba', 'bb', 'bd', 'be', 'bf',
30             'bg', 'bh', 'bi', 'bj', 'bm', 'bn', 'bo', 'br', 'bs', 'bt', 'bv', 'bw',
31             'by', 'bz', 'ca', 'cc', 'cd', 'cf', 'cg', 'ch', 'ci', 'ck', 'cl', 'cm',
32             'cn', 'co', 'cr', 'cu', 'cv', 'cx', 'cy', 'cz', 'de', 'dj', 'dk', 'dm',
33             'do', 'dz', 'ec', 'ee', 'eg', 'eh', 'er', 'es', 'et', 'fi', 'fj', 'fk',
34             'fm', 'fo', 'fr', 'sx', 'ga', 'gb', 'gd', 'ge', 'gf', 'gh', 'gi', 'gl',
35             'gm', 'gn', 'gp', 'gq', 'gr', 'gs', 'gt', 'gu', 'gw', 'gy', 'hk', 'hm',
36             'hn', 'hr', 'ht', 'hu', 'id', 'ie', 'il', 'in', 'io', 'iq', 'ir', 'is',
37             'it', 'jm', 'jo', 'jp', 'ke', 'kg', 'kh', 'ki', 'km', 'kn', 'kp', 'kr',
38             'kw', 'ky', 'kz', 'la', 'lb', 'lc', 'li', 'lk', 'lr', 'ls', 'lt', 'lu',
39             'lv', 'ly', 'ma', 'mc', 'md', 'mg', 'mh', 'mk', 'ml', 'mm', 'mn', 'mo',
40             'mp', 'mq', 'mr', 'ms', 'mt', 'mu', 'mv', 'mw', 'mx', 'my', 'mz', 'na',
41             'nc', 'ne', 'nf', 'ng', 'ni', 'nl', 'no', 'np', 'nr', 'nu', 'nz', 'om',
42             'pa', 'pe', 'pf', 'pg', 'ph', 'pk', 'pl', 'pm', 'pn', 'pr', 'ps', 'pt',
43             'pw', 'py', 'qa', 're', 'ro', 'ru', 'rw', 'sa', 'sb', 'sc', 'sd', 'se',
44             'sg', 'sh', 'si', 'sj', 'sk', 'sl', 'sm', 'sn', 'so', 'sr', 'st', 'sv',
45             'sy', 'sz', 'tc', 'td', 'tf', 'tg', 'th', 'tj', 'tk', 'tm', 'tn', 'to',
46             'tl', 'tr', 'tt', 'tv', 'tw', 'tz', 'ua', 'ug', 'um', 'us', 'uy', 'uz',
47             'va', 'vc', 've', 'vg', 'vi', 'vn', 'vu', 'wf', 'ws', 'ye', 'yt', 'rs',
48             'za', 'zm', 'me', 'zw', 'a1', 'xk', 'o1', 'ax', 'gg', 'im', 'je', 'bl',
49             'mf', 'bq', 'ss'
50             );
51              
52             sub new {
53 1     1 0 16 my $class = shift;
54 1         3 my $file = shift;
55              
56 1         8 my $self = fields::new( $class );
57              
58 1 50       4321 open( my $fl, $file ) || croak( 'Could not open db file' );
59 1         8 binmode $fl, ':bytes';
60              
61 1         23 read $fl, my $header, HEADER_LENGTH;
62 1 50       7 croak 'File format is wrong' if substr( $header, 0, 3 ) ne 'SxG';
63              
64 1         4 my $info_str = substr( $header, 3, HEADER_LENGTH - 3 );
65 1         12 my @info = unpack 'CNCCCnnNCnnNNnNn', $info_str;
66 1 50       14 croak 'File header format is wrong' if $info[4] * $info[5] * $info[6] * $info[7] * $info[1] * $info[8] == 0;
67              
68 1 50       5 if ( $info[15] ) {
69 1         3 read $fl, my $pack, $info[15];
70 1         9 $self->{pack} = [ split "\0", $pack ];
71             }
72              
73 1         5 read $fl, $self->{b_idx_str}, $info[4] * 4;
74 1         17 read $fl, $self->{m_idx_str}, $info[5] * 4;
75              
76 1         3 $self->{range} = $info[6];
77 1         3 $self->{b_idx_len} = $info[4];
78 1         3 $self->{m_idx_len} = $info[5];
79 1         2 $self->{db_items} = $info[7];
80 1         3 $self->{id_len} = $info[8];
81 1         4 $self->{block_len} = 3 + $self->{id_len};
82 1         2 $self->{max_region} = $info[9];
83 1         3 $self->{max_city} = $info[10];
84 1         2 $self->{max_country} = $info[13];
85 1         4 $self->{country_size} = $info[14];
86              
87 1         6 $self->{db_begin} = tell $fl;
88              
89 1         5 $self->{regions_begin} = $self->{db_begin} + $self->{db_items} * $self->{block_len};
90 1         2 $self->{cities_begin} = $self->{regions_begin} + $info[11];
91              
92 1         3 $self->{db_file} = $file;
93              
94 1         8 close $fl;
95              
96 1         8 return $self;
97             }
98              
99             sub get_city {
100 1     1 0 2 my __PACKAGE__ $self = shift;
101 1         2 my $ip = shift;
102 1         1 my $lang = shift;
103              
104 1         3 my $seek = $self->get_num($ip);
105 1 50       4 return unless $seek;
106              
107 1         5 my $info = $self->parse_info( $seek, $lang );
108 1 50       5 return unless $info;
109              
110 1         3 my $city;
111 1 50 33     6 if ( $lang && $lang eq 'en' ) {
112 0         0 $city = $info->[6];
113             }
114             else {
115 1         3 $city = $info->[5];
116             }
117 1 50       4 return unless $city;
118              
119 1         5 return decode_utf8($city);
120             }
121              
122             sub get_country {
123 1     1 0 518 my __PACKAGE__ $self = shift;
124 1         2 my $ip = shift;
125              
126 1         5 my $seek = $self->get_num($ip);
127 1 50       8 return unless $seek;
128              
129 1         5 my $info = $self->parse_info($seek);
130 1 50       6 return unless $info;
131              
132 1         2 my $country;
133 1 50       8 if ( $info->[1] =~ /\D/ ) {
134 0         0 $country = $info->[1];
135             }
136             else {
137 1         4 $country = $COUNTRY_ISO_MAP[ $info->[1] ];
138             }
139              
140 1         5 return $country;
141             }
142              
143             sub parse {
144 1     1 0 21 my __PACKAGE__ $self = shift;
145 1         3 my $ip = shift;
146 1         2 my $lang = shift;
147 1         9 my $seek = $self->get_num($ip);
148 1 50       7 return unless $seek;
149              
150 1         6 my $info = $self->parse_info($seek, $lang);
151 1         11 return Geo::SypexGeo::Info->new($info, $lang);
152             }
153              
154             sub get_num {
155 3     3 0 6 my __PACKAGE__ $self = shift;
156 3         5 my $ip = shift;
157              
158 3         4 my $ip1n;
159             {
160 2     2   12 no warnings;
  2         2  
  2         2511  
  3         4  
161 3         16 $ip1n = int $ip;
162             }
163              
164 3 50 33     41 return undef if !$ip1n || $ip1n == 10 || $ip1n == 127 || $ip1n >= $self->{b_idx_len};
      33        
      33        
165 3         11 my $ipn = ip2long( $ip );
166 3         11 $ipn = pack( 'N', $ipn );
167              
168 3         18 my @blocks = unpack "NN", substr( $self->{b_idx_str} , ( $ip1n - 1 ) * 4, 8 );
169              
170 3         5 my $min;
171             my $max;
172              
173 3 50       12 if ( $blocks[1] - $blocks[0] > $self->{range} ) {
174             my $part = $self->search_idx(
175             $ipn,
176             floor( $blocks[0] / $self->{'range'} ),
177 3         41 floor( $blocks[1] / $self->{'range'} ) - 1
178             );
179              
180 3 50       13 $min = $part > 0 ? $part * $self->{range} : 0;
181 3 50       11 $max = $part > $self->{m_idx_len} ? $self->{db_items} : ( $part + 1 ) * $self->{range};
182              
183 3 50       17 $min = $blocks[0] if $min < $blocks[0];
184 3 50       11 $max = $blocks[1] if $max > $blocks[1];
185             }
186             else {
187 0         0 $min = $blocks[0];
188 0         0 $max = $blocks[1];
189             }
190              
191 3         5 my $len = $max - $min;
192              
193 3 50       115 open( my $fl, $self->{ 'db_file' } ) || croak( 'Could not open db file' );
194 3         15 binmode $fl, ':bytes';
195 3         13 seek $fl, $self->{db_begin} + $min * $self->{block_len}, 0;
196 3         57 read $fl, my $buf, $len * $self->{block_len};
197 3         22 close $fl;
198              
199 3         13 return $self->search_db( $buf, $ipn, 0, $len - 1 );
200             }
201              
202             sub search_idx {
203 3     3 0 4 my __PACKAGE__ $self = shift;
204 3         6 my $ipn = shift;
205 3         3 my $min = shift;
206 3         4 my $max = shift;
207              
208 3         4 my $offset;
209 3         12 while ( $max - $min > 8 ) {
210 6         6 $offset = ( $min + $max ) >> 1;
211              
212 6 50       19 if ( encode_utf8($ipn) gt encode_utf8( substr( ( $self->{m_idx_str} ), $offset * 4, 4 ) ) ) {
213 6         89 $min = $offset;
214             }
215             else {
216 0         0 $max = $offset;
217             }
218             }
219              
220 3   66     9 while ( encode_utf8($ipn) gt encode_utf8( substr( $self->{m_idx_str}, $min * 4, 4 ) ) && $min++ < $max ) {
221             }
222              
223 3         286 return $min;
224             }
225              
226             sub search_db {
227 3     3 0 5 my __PACKAGE__ $self = shift;
228 3         4 my $str = shift;
229 3         5 my $ipn = shift;
230 3         4 my $min = shift;
231 3         3 my $max = shift;
232              
233 3 50       8 if( $max - $min > 1 ) {
234 3         6 $ipn = substr( $ipn, 1 );
235 3         3 my $offset;
236 3         9 while ( $max - $min > 8 ){
237 18         19 $offset = ( $min + $max ) >> 1;
238              
239 18 100       29 if ( encode_utf8( $ipn ) gt encode_utf8( substr( $str, $offset * $self->{block_len}, 3 ) ) ) {
240 9         77 $min = $offset;
241             }
242             else {
243 9         91 $max = $offset;
244             }
245             }
246              
247 3   66     8 while ( encode_utf8( $ipn ) ge encode_utf8( substr( $str, $min * $self->{block_len}, 3 ) ) && $min++ < $max ){}
248             }
249             else {
250 0         0 return hex( bin2hex( substr( $str, $min * $self->{block_len} + 3 , 3 ) ) );
251             }
252              
253 3         139 return hex( bin2hex( substr( $str, $min * $self->{block_len} - $self->{id_len}, $self->{id_len} ) ) );
254             }
255              
256             sub bin2hex {
257 3     3 0 6 my $str = shift;
258              
259 3         6 my $res = '';
260 3         8 for my $i ( 0 .. length( $str ) - 1 ) {
261 9         37 $res .= sprintf( '%02s', sprintf( '%x', ord( substr( $str, $i, 1 ) ) ) );
262             }
263              
264 3         22 return $res;
265             }
266              
267             sub ip2long {
268 3     3 0 48 return unpack( 'l*', pack( 'l*', unpack( 'N*', inet_aton( shift ) ) ) );
269             }
270              
271             sub parse_info {
272 3     3 0 5 my __PACKAGE__ $self = shift;
273 3         3 my $seek = shift;
274              
275 3         4 my $info;
276              
277 3 50       11 if ( $seek < $self->{country_size} ) {
278 0 0       0 open( my $fl, $self->{db_file} ) || croak('Could not open db file');
279 0         0 binmode $fl, ':bytes';
280 0         0 seek $fl, $seek + $self->{cities_begin}, 0;
281 0         0 read $fl, my $buf, $self->{max_country};
282 0         0 close $fl;
283              
284 0         0 $info = extended_unpack( $self->{pack}[0], $buf );
285             }
286             else {
287 3 50       99 open( my $fl, $self->{db_file} ) || croak('Could not open db file');
288 3         16 binmode $fl, ':bytes';
289 3         10 seek $fl, $seek + $self->{cities_begin}, 0;
290 3         25 read $fl, my $buf, $self->{max_city};
291 3         19 close $fl;
292              
293 3         10 $info = extended_unpack( $self->{pack}[2], $buf );
294             }
295              
296 3 50       9 if ($info) {
297 3         8 return $info;
298             }
299             else {
300 0         0 return;
301             }
302             }
303              
304             sub extended_unpack {
305 3     3 0 5 my $flags = shift;
306 3         3 my $val = shift;
307              
308 3         5 my $pos = 0;
309 3         5 my $result = [];
310              
311 3         15 my @flags_arr = split '/', $flags;
312              
313 3         5 foreach my $flag_str ( @flags_arr ) {
314 21         53 my ( $type, $name ) = split ':', $flag_str;
315              
316 21         30 my $flag = substr $type, 0, 1;
317 21         23 my $num = substr $type, 1, 1;
318              
319 21         14 my $len;
320              
321 21 50       103 if ( $flag eq 't' ) {
    100          
    50          
    50          
    50          
    50          
    100          
    50          
    50          
    100          
322             }
323             elsif ( $flag eq 'T' ) {
324 3         4 $len = 1;
325             }
326             elsif ( $flag eq 's' ) {
327             }
328             elsif ( $flag eq 'n' ) {
329 0         0 $len = $num;
330             }
331             elsif ( $flag eq 'S' ) {
332 0         0 $len = 2;
333             }
334             elsif ( $flag eq 'm' ) {
335             }
336             elsif ( $flag eq 'M' ) {
337 6         6 $len = 3;
338             }
339             elsif ( $flag eq 'd' ) {
340 0         0 $len = 8;
341             }
342             elsif ( $flag eq 'c' ) {
343 0         0 $len = $num;
344             }
345             elsif ( $flag eq 'b' ) {
346 6         46 $len = index( $val, "\0", $pos ) - $pos;
347             }
348             else {
349 6         7 $len = 4;
350             }
351              
352 21         28 my $subval = substr( $val, $pos, $len );
353              
354 21         13 my $res;
355              
356 21 50       158 if ( $flag eq 't' ) {
    100          
    50          
    50          
    50          
    100          
    50          
    50          
    50          
    50          
    50          
    100          
    50          
    50          
357 0         0 $res = ( unpack 'c', $subval )[0];
358             }
359             elsif ( $flag eq 'T' ) {
360 3         7 $res = ( unpack 'C', $subval )[0];
361             }
362             elsif ( $flag eq 's' ) {
363 0         0 $res = ( unpack 's', $subval )[0];
364             }
365             elsif ( $flag eq 'S' ) {
366 0         0 $res = ( unpack 'S', $subval )[0];
367             }
368             elsif ( $flag eq 'm' ) {
369 0 0       0 $res = ( unpack 'l', $subval . ( ord( substr( $subval, 2, 1 ) ) >> 7 ? "\xff" : "\0" ) )[0];
370             }
371             elsif ( $flag eq 'M' ) {
372 6         16 $res = ( unpack 'L', $subval . "\0" )[0];
373             }
374             elsif ( $flag eq 'i' ) {
375 0         0 $res = ( unpack 'l', $subval )[0];
376             }
377             elsif ( $flag eq 'I' ) {
378 0         0 $res = ( unpack 'L', $subval )[0];
379             }
380             elsif ( $flag eq 'f' ) {
381 0         0 $res = ( unpack 'f', $subval )[0];
382             }
383             elsif ( $flag eq 'd' ) {
384 0         0 $res = ( unpack 'd', $subval )[0];
385             }
386             elsif ( $flag eq 'n' ) {
387 0         0 $res = ( unpack 's', $subval )[0] / ( 10 ** $num );
388             }
389             elsif ( $flag eq 'N' ) {
390 6         19 $res = ( unpack 'l', $subval )[0] / ( 10 ** $num );
391             }
392             elsif ( $flag eq 'c' ) {
393 0         0 $res = rtrim $subval;
394             }
395             elsif ( $flag eq 'b' ) {
396 6         9 $res = $subval;
397 6         6 $len++;
398             }
399              
400 21         18 $pos += $len;
401              
402 21         43 push @$result, $res;
403             }
404              
405 3         17 return $result;
406             }
407              
408             1;
409              
410             =head1 NAME
411              
412             Geo::SypexGeo - API to detect cities by IP thru Sypex Geo database v.2
413              
414             =head1 SYNOPSIS
415              
416             use Geo::SypexGeo;
417             my $geo = Geo::SypexGeo->new( './SxGeoCity.dat' );
418              
419             # Method parse return Geo::SypexGeo::Info object
420             $info = $geo->parse( '87.250.250.203', 'en' )
421             or die "Cant parse 87.250.250.203";
422             say $info->city();
423              
424             $info = $geo->parse('93.191.14.81') or die "Cant parse 93.191.14.81";
425             say $info->city();
426             say $info->country();
427              
428             my ( $latitude, $longitude ) = $info->coordinates();
429             say "Latitude: $latitude Longitude: $longitude";
430              
431             ## deprecated method (will be removed in future versions)
432             say $geo->get_city( '87.250.250.203', 'en' );
433              
434             ## deprecated method (will be removed in future versions)
435             say $geo->get_city('93.191.14.81');
436              
437             ## deprecated method (will be removed in future versions)
438             say $geo->get_country('93.191.14.81');
439              
440             =head1 DESCRIPTION
441              
442             L is a database to detect cities by IP.
443              
444             The database of IPs is included into distribution, but it is better to download latest version at L.
445              
446             The database is availible with a names of the cities in Russian and English languages.
447              
448             This module now is detect only city name and don't use any features to speed up of detection. In the future I plan to add more functionality.
449              
450             =head1 SOURCE AVAILABILITY
451              
452             The source code for this module is available from Github
453             at https://github.com/kak-tus/Geo-SypexGeo
454              
455             =head1 AUTHOR
456              
457             Andrey Kuzmin, Ekak-tus@mail.ruE
458              
459             =head1 CREDITS
460              
461             vrag86
462             dimonchik-com
463              
464             =head1 COPYRIGHT AND LICENSE
465              
466             Copyright (C) 2014 by Andrey Kuzmin
467              
468             This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
469              
470             =cut