File Coverage

blib/lib/Geo/SypexGeo.pm
Criterion Covered Total %
statement 189 215 87.9
branch 55 100 55.0
condition 8 18 44.4
subroutine 24 24 100.0
pod 0 11 0.0
total 276 368 75.0


line stmt bran cond sub pod time code
1             package Geo::SypexGeo;
2              
3             our $VERSION = '0.6';
4              
5 2     2   13381 use strict;
  2         3  
  2         44  
6 2     2   6 use warnings;
  2         1  
  2         37  
7 2     2   10 use utf8;
  2         2  
  2         6  
8 2     2   49 use v5.10;
  2         4  
9              
10 2     2   8 use Carp qw( croak );
  2         2  
  2         89  
11 2     2   910 use Encode;
  2         12964  
  2         106  
12 2     2   985 use Socket;
  2         5495  
  2         647  
13 2     2   815 use POSIX;
  2         7973  
  2         7  
14 2     2   4226 use Text::Trim;
  2         793  
  2         89  
15 2     2   614 use Geo::SypexGeo::Info;
  2         3  
  2         62  
16              
17 2         8 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   850 );
  2         1951  
22              
23             use constant {
24 2         1178 HEADER_LENGTH => 40,
25 2     2   177 };
  2         2  
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 7 my $class = shift;
54 1         2 my $file = shift;
55              
56 1         3 my $self = fields::new( $class );
57              
58 1 50       2498 open( my $fl, $file ) || croak( 'Could not open db file' );
59 1         5 binmode $fl, ':bytes';
60              
61 1         15 read $fl, my $header, HEADER_LENGTH;
62 1 50       4 croak 'File format is wrong' if substr( $header, 0, 3 ) ne 'SxG';
63              
64 1         2 my $info_str = substr( $header, 3, HEADER_LENGTH - 3 );
65 1         6 my @info = unpack 'CNCCCnnNCnnNNnNn', $info_str;
66 1 50       7 croak 'File header format is wrong' if $info[4] * $info[5] * $info[6] * $info[7] * $info[1] * $info[8] == 0;
67              
68 1 50       3 if ( $info[15] ) {
69 1         1 read $fl, my $pack, $info[15];
70 1         6 $self->{pack} = [ split "\0", $pack ];
71             }
72              
73 1         3 read $fl, $self->{b_idx_str}, $info[4] * 4;
74 1         10 read $fl, $self->{m_idx_str}, $info[5] * 4;
75              
76 1         2 $self->{range} = $info[6];
77 1         1 $self->{b_idx_len} = $info[4];
78 1         2 $self->{m_idx_len} = $info[5];
79 1         1 $self->{db_items} = $info[7];
80 1         2 $self->{id_len} = $info[8];
81 1         2 $self->{block_len} = 3 + $self->{id_len};
82 1         2 $self->{max_region} = $info[9];
83 1         1 $self->{max_city} = $info[10];
84 1         2 $self->{max_country} = $info[13];
85 1         1 $self->{country_size} = $info[14];
86              
87 1         4 $self->{db_begin} = tell $fl;
88              
89 1         2 $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         1 $self->{db_file} = $file;
93              
94 1         6 close $fl;
95              
96 1         4 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         2 my $seek = $self->get_num($ip);
105 1 50       3 return unless $seek;
106              
107 1         3 my $info = $self->parse_info( $seek, $lang );
108 1 50       3 return unless $info;
109              
110 1         1 my $city;
111 1 50 33     3 if ( $lang && $lang eq 'en' ) {
112 0         0 $city = $info->[6];
113             }
114             else {
115 1         2 $city = $info->[5];
116             }
117 1 50       2 return unless $city;
118              
119 1         3 return decode_utf8($city);
120             }
121              
122             sub get_country {
123 1     1 0 246 my __PACKAGE__ $self = shift;
124 1         1 my $ip = shift;
125              
126 1         3 my $seek = $self->get_num($ip);
127 1 50       4 return unless $seek;
128              
129 1         3 my $info = $self->parse_info($seek);
130 1 50       3 return unless $info;
131              
132 1         2 my $country = $COUNTRY_ISO_MAP[ $info->[1] ];
133 1         2 return $country;
134             }
135              
136             sub parse {
137 1     1 0 5 my __PACKAGE__ $self = shift;
138 1         2 my $ip = shift;
139 1         1 my $lang = shift;
140 1         4 my $seek = $self->get_num($ip);
141 1 50       3 return unless $seek;
142              
143 1         3 my $info = $self->parse_info($seek, $lang);
144 1         7 return Geo::SypexGeo::Info->new($info, $lang);
145             }
146              
147             sub get_num {
148 3     3 0 3 my __PACKAGE__ $self = shift;
149 3         3 my $ip = shift;
150              
151 3         2 my $ip1n;
152             {
153 2     2   8 no warnings;
  2         2  
  2         1955  
  3         2  
154 3         9 $ip1n = int $ip;
155             }
156              
157 3 50 33     25 return undef if !$ip1n || $ip1n == 10 || $ip1n == 127 || $ip1n >= $self->{b_idx_len};
      33        
      33        
158 3         5 my $ipn = ip2long( $ip );
159 3         7 $ipn = pack( 'N', $ipn );
160              
161 3         8 my @blocks = unpack "NN", substr( $self->{b_idx_str} , ( $ip1n - 1 ) * 4, 8 );
162              
163 3         3 my $min;
164             my $max;
165              
166 3 50       5 if ( $blocks[1] - $blocks[0] > $self->{range} ) {
167             my $part = $self->search_idx(
168             $ipn,
169             floor( $blocks[0] / $self->{'range'} ),
170 3         22 floor( $blocks[1] / $self->{'range'} ) - 1
171             );
172              
173 3 50       6 $min = $part > 0 ? $part * $self->{range} : 0;
174 3 50       6 $max = $part > $self->{m_idx_len} ? $self->{db_items} : ( $part + 1 ) * $self->{range};
175              
176 3 50       4 $min = $blocks[0] if $min < $blocks[0];
177 3 50       11 $max = $blocks[1] if $max > $blocks[1];
178             }
179             else {
180 0         0 $min = $blocks[0];
181 0         0 $max = $blocks[1];
182             }
183              
184 3         2 my $len = $max - $min;
185              
186 3 50       67 open( my $fl, $self->{ 'db_file' } ) || croak( 'Could not open db file' );
187 3         9 binmode $fl, ':bytes';
188 3         7 seek $fl, $self->{db_begin} + $min * $self->{block_len}, 0;
189 3         35 read $fl, my $buf, $len * $self->{block_len};
190 3         14 close $fl;
191              
192 3         9 return $self->search_db( $buf, $ipn, 0, $len - 1 );
193             }
194              
195             sub search_idx {
196 3     3 0 2 my __PACKAGE__ $self = shift;
197 3         3 my $ipn = shift;
198 3         2 my $min = shift;
199 3         2 my $max = shift;
200              
201 3         3 my $offset;
202 3         6 while ( $max - $min > 8 ) {
203 6         4 $offset = ( $min + $max ) >> 1;
204              
205 6 50       11 if ( encode_utf8($ipn) gt encode_utf8( substr( ( $self->{m_idx_str} ), $offset * 4, 4 ) ) ) {
206 6         48 $min = $offset;
207             }
208             else {
209 0         0 $max = $offset;
210             }
211             }
212              
213 3   66     5 while ( encode_utf8($ipn) gt encode_utf8( substr( $self->{m_idx_str}, $min * 4, 4 ) ) && $min++ < $max ) {
214             }
215              
216 3         165 return $min;
217             }
218              
219             sub search_db {
220 3     3 0 3 my __PACKAGE__ $self = shift;
221 3         3 my $str = shift;
222 3         4 my $ipn = shift;
223 3         2 my $min = shift;
224 3         2 my $max = shift;
225              
226 3 50       6 if( $max - $min > 1 ) {
227 3         4 $ipn = substr( $ipn, 1 );
228 3         3 my $offset;
229 3         5 while ( $max - $min > 8 ){
230 18         14 $offset = ( $min + $max ) >> 1;
231              
232 18 100       22 if ( encode_utf8( $ipn ) gt encode_utf8( substr( $str, $offset * $self->{block_len}, 3 ) ) ) {
233 9         50 $min = $offset;
234             }
235             else {
236 9         59 $max = $offset;
237             }
238             }
239              
240 3   66     5 while ( encode_utf8( $ipn ) ge encode_utf8( substr( $str, $min * $self->{block_len}, 3 ) ) && $min++ < $max ){}
241             }
242             else {
243 0         0 return hex( bin2hex( substr( $str, $min * $self->{block_len} + 3 , 3 ) ) );
244             }
245              
246 3         89 return hex( bin2hex( substr( $str, $min * $self->{block_len} - $self->{id_len}, $self->{id_len} ) ) );
247             }
248              
249             sub bin2hex {
250 3     3 0 3 my $str = shift;
251              
252 3         3 my $res = '';
253 3         7 for my $i ( 0 .. length( $str ) - 1 ) {
254 9         20 $res .= sprintf( '%02s', sprintf( '%x', ord( substr( $str, $i, 1 ) ) ) );
255             }
256              
257 3         12 return $res;
258             }
259              
260             sub ip2long {
261 3     3 0 25 return unpack( 'l*', pack( 'l*', unpack( 'N*', inet_aton( shift ) ) ) );
262             }
263              
264             sub parse_info {
265 3     3 0 4 my __PACKAGE__ $self = shift;
266 3         2 my $seek = shift;
267              
268 3         2 my $info;
269              
270 3 50       6 if ( $seek < $self->{country_size} ) {
271 0 0       0 open( my $fl, $self->{db_file} ) || croak('Could not open db file');
272 0         0 binmode $fl, ':bytes';
273 0         0 seek $fl, $seek + $self->{cities_begin}, 0;
274 0         0 read $fl, my $buf, $self->{max_country};
275 0         0 close $fl;
276              
277 0         0 $info = extended_unpack( $self->{pack}[0], $buf );
278             }
279             else {
280 3 50       59 open( my $fl, $self->{db_file} ) || croak('Could not open db file');
281 3         8 binmode $fl, ':bytes';
282 3         7 seek $fl, $seek + $self->{cities_begin}, 0;
283 3         19 read $fl, my $buf, $self->{max_city};
284 3         12 close $fl;
285              
286 3         7 $info = extended_unpack( $self->{pack}[2], $buf );
287             }
288              
289 3 50       7 if ($info) {
290 3         5 return $info;
291             }
292             else {
293 0         0 return;
294             }
295             }
296              
297             sub extended_unpack {
298 3     3 0 3 my $flags = shift;
299 3         3 my $val = shift;
300              
301 3         3 my $pos = 0;
302 3         4 my $result = [];
303              
304 3         11 my @flags_arr = split '/', $flags;
305              
306 3         5 foreach my $flag_str ( @flags_arr ) {
307 21         57 my ( $type, $name ) = split ':', $flag_str;
308              
309 21         23 my $flag = substr $type, 0, 1;
310 21         15 my $num = substr $type, 1, 1;
311              
312 21         13 my $len;
313              
314 21 50       72 if ( $flag eq 't' ) {
    100          
    50          
    50          
    50          
    50          
    100          
    50          
    50          
    100          
315             }
316             elsif ( $flag eq 'T' ) {
317 3         2 $len = 1;
318             }
319             elsif ( $flag eq 's' ) {
320             }
321             elsif ( $flag eq 'n' ) {
322 0         0 $len = $num;
323             }
324             elsif ( $flag eq 'S' ) {
325 0         0 $len = 2;
326             }
327             elsif ( $flag eq 'm' ) {
328             }
329             elsif ( $flag eq 'M' ) {
330 6         4 $len = 3;
331             }
332             elsif ( $flag eq 'd' ) {
333 0         0 $len = 8;
334             }
335             elsif ( $flag eq 'c' ) {
336 0         0 $len = $num;
337             }
338             elsif ( $flag eq 'b' ) {
339 6         32 $len = index( $val, "\0", $pos ) - $pos;
340             }
341             else {
342 6         4 $len = 4;
343             }
344              
345 21         19 my $subval = substr( $val, $pos, $len );
346              
347 21         11 my $res;
348              
349 21 50       90 if ( $flag eq 't' ) {
    100          
    50          
    50          
    50          
    100          
    50          
    50          
    50          
    50          
    50          
    100          
    50          
    50          
350 0         0 $res = ( unpack 'c', $subval )[0];
351             }
352             elsif ( $flag eq 'T' ) {
353 3         4 $res = ( unpack 'C', $subval )[0];
354             }
355             elsif ( $flag eq 's' ) {
356 0         0 $res = ( unpack 's', $subval )[0];
357             }
358             elsif ( $flag eq 'S' ) {
359 0         0 $res = ( unpack 'S', $subval )[0];
360             }
361             elsif ( $flag eq 'm' ) {
362 0 0       0 $res = ( unpack 'l', $subval . ( ord( substr( $subval, 2, 1 ) ) >> 7 ? "\xff" : "\0" ) )[0];
363             }
364             elsif ( $flag eq 'M' ) {
365 6         11 $res = ( unpack 'L', $subval . "\0" )[0];
366             }
367             elsif ( $flag eq 'i' ) {
368 0         0 $res = ( unpack 'l', $subval )[0];
369             }
370             elsif ( $flag eq 'I' ) {
371 0         0 $res = ( unpack 'L', $subval )[0];
372             }
373             elsif ( $flag eq 'f' ) {
374 0         0 $res = ( unpack 'f', $subval )[0];
375             }
376             elsif ( $flag eq 'd' ) {
377 0         0 $res = ( unpack 'd', $subval )[0];
378             }
379             elsif ( $flag eq 'n' ) {
380 0         0 $res = ( unpack 's', $subval )[0] / ( 10 ** $num );
381             }
382             elsif ( $flag eq 'N' ) {
383 6         13 $res = ( unpack 'l', $subval )[0] / ( 10 ** $num );
384             }
385             elsif ( $flag eq 'c' ) {
386 0         0 $res = rtrim $subval;
387             }
388             elsif ( $flag eq 'b' ) {
389 6         4 $res = $subval;
390 6         6 $len++;
391             }
392              
393 21         10 $pos += $len;
394              
395 21         32 push @$result, $res;
396             }
397              
398 3         9 return $result;
399             }
400              
401             1;
402              
403             =head1 NAME
404              
405             Geo::SypexGeo - API to detect cities by IP thru Sypex Geo database v.2
406              
407             =head1 SYNOPSIS
408              
409             use Geo::SypexGeo;
410             my $geo = Geo::SypexGeo->new( './SxGeoCity.dat' );
411              
412             # Method parse return Geo::SypexGeo::Info object
413             $info = $geo->parse( '87.250.250.203', 'en' )
414             or die "Cant parse 87.250.250.203";
415             say $info->city();
416              
417             $info = $geo->parse('93.191.14.81') or die "Cant parse 93.191.14.81";
418             say $info->city();
419             say $info->country();
420              
421             my ( $latitude, $longitude ) = $info->coordinates();
422             say "Latitude: $latitude Longitude: $longitude";
423              
424             ## deprecated method (will be removed in future versions)
425             say $geo->get_city( '87.250.250.203', 'en' );
426              
427             ## deprecated method (will be removed in future versions)
428             say $geo->get_city('93.191.14.81');
429              
430             ## deprecated method (will be removed in future versions)
431             say $geo->get_country('93.191.14.81');
432              
433             =head1 DESCRIPTION
434              
435             L is a database to detect cities by IP.
436              
437             The database of IPs is included into distribution, but it is better to download latest version at L.
438              
439             The database is availible with a names of the cities in Russian and English languages.
440              
441             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.
442              
443             =head1 SOURCE AVAILABILITY
444              
445             The source code for this module is available from Github
446             at https://github.com/kak-tus/Geo-SypexGeo
447              
448             =head1 AUTHOR
449              
450             Andrey Kuzmin, Ekak-tus@mail.ruE
451              
452             =head1 CREDITS
453              
454             vrag86
455              
456             =head1 COPYRIGHT AND LICENSE
457              
458             Copyright (C) 2014 by Andrey Kuzmin
459              
460             This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
461              
462             =cut