File Coverage

blib/lib/Geo/SypexGeo.pm
Criterion Covered Total %
statement 27 29 93.1
branch n/a
condition n/a
subroutine 10 10 100.0
pod n/a
total 37 39 94.8


line stmt bran cond sub pod time code
1             package Geo::SypexGeo;
2              
3             our $VERSION = '0.5';
4              
5 2     2   16443 use strict;
  2         3  
  2         56  
6 2     2   7 use warnings;
  2         2  
  2         43  
7 2     2   14 use utf8;
  2         1  
  2         8  
8 2     2   59 use v5.10;
  2         9  
9              
10 2     2   8 use Carp qw( croak );
  2         2  
  2         143  
11 2     2   1124 use Encode;
  2         16383  
  2         137  
12 2     2   1139 use Socket;
  2         6627  
  2         801  
13 2     2   1041 use POSIX;
  2         9884  
  2         9  
14 2     2   5254 use Text::Trim;
  2         981  
  2         112  
15 2     2   1180 use Geo::SypexGeo::Info;
  0            
  0            
16              
17             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             );
22              
23             use constant {
24             HEADER_LENGTH => 40,
25             };
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             my $class = shift;
54             my $file = shift;
55              
56             my $self = fields::new( $class );
57              
58             open( my $fl, $file ) || croak( 'Could not open db file' );
59             binmode $fl, ':bytes';
60              
61             read $fl, my $header, HEADER_LENGTH;
62             croak 'File format is wrong' if substr( $header, 0, 3 ) ne 'SxG';
63              
64             my $info_str = substr( $header, 3, HEADER_LENGTH - 3 );
65             my @info = unpack 'CNCCCnnNCnnNNnNn', $info_str;
66             croak 'File header format is wrong' if $info[4] * $info[5] * $info[6] * $info[7] * $info[1] * $info[8] == 0;
67              
68             if ( $info[15] ) {
69             read $fl, my $pack, $info[15];
70             $self->{pack} = [ split "\0", $pack ];
71             }
72              
73             read $fl, $self->{b_idx_str}, $info[4] * 4;
74             read $fl, $self->{m_idx_str}, $info[5] * 4;
75              
76             $self->{range} = $info[6];
77             $self->{b_idx_len} = $info[4];
78             $self->{m_idx_len} = $info[5];
79             $self->{db_items} = $info[7];
80             $self->{id_len} = $info[8];
81             $self->{block_len} = 3 + $self->{id_len};
82             $self->{max_region} = $info[9];
83             $self->{max_city} = $info[10];
84             $self->{max_country} = $info[13];
85             $self->{country_size} = $info[14];
86              
87             $self->{db_begin} = tell $fl;
88              
89             $self->{regions_begin} = $self->{db_begin} + $self->{db_items} * $self->{block_len};
90             $self->{cities_begin} = $self->{regions_begin} + $info[11];
91              
92             $self->{db_file} = $file;
93              
94             close $fl;
95              
96             return $self;
97             }
98              
99             sub get_city {
100             my __PACKAGE__ $self = shift;
101             my $ip = shift;
102             my $lang = shift;
103              
104             my $seek = $self->get_num($ip);
105             return unless $seek;
106              
107             my $info = $self->parse_info( $seek, $lang );
108             return unless $info;
109              
110             my $city;
111             if ( $lang && $lang eq 'en' ) {
112             $city = $info->[6];
113             }
114             else {
115             $city = $info->[5];
116             }
117             return unless $city;
118              
119             return decode_utf8($city);
120             }
121              
122             sub get_country {
123             my __PACKAGE__ $self = shift;
124             my $ip = shift;
125              
126             my $seek = $self->get_num($ip);
127             return unless $seek;
128              
129             my $info = $self->parse_info($seek);
130             return unless $info;
131              
132             my $country = $COUNTRY_ISO_MAP[ $info->[1] ];
133             return $country;
134             }
135              
136             sub parse {
137             my __PACKAGE__ $self = shift;
138             my $ip = shift;
139             my $lang = shift;
140             my $seek = $self->get_num($ip);
141             return unless $seek;
142              
143             my $info = $self->parse_info($seek, $lang);
144             return Geo::SypexGeo::Info->new($info, $lang);
145             }
146              
147             sub get_num {
148             my __PACKAGE__ $self = shift;
149             my $ip = shift;
150              
151             my $ip1n;
152             {
153             no warnings;
154             $ip1n = int $ip;
155             }
156              
157             return undef if !$ip1n || $ip1n == 10 || $ip1n == 127 || $ip1n >= $self->{b_idx_len};
158             my $ipn = ip2long( $ip );
159             $ipn = pack( 'N', $ipn );
160              
161             my @blocks = unpack "NN", substr( $self->{b_idx_str} , ( $ip1n - 1 ) * 4, 8 );
162              
163             my $min;
164             my $max;
165              
166             if ( $blocks[1] - $blocks[0] > $self->{range} ) {
167             my $part = $self->search_idx(
168             $ipn,
169             floor( $blocks[0] / $self->{'range'} ),
170             floor( $blocks[1] / $self->{'range'} ) - 1
171             );
172              
173             $min = $part > 0 ? $part * $self->{range} : 0;
174             $max = $part > $self->{m_idx_len} ? $self->{db_items} : ( $part + 1 ) * $self->{range};
175              
176             $min = $blocks[0] if $min < $blocks[0];
177             $max = $blocks[1] if $max > $blocks[1];
178             }
179             else {
180             $min = $blocks[0];
181             $max = $blocks[1];
182             }
183              
184             my $len = $max - $min;
185              
186             open( my $fl, $self->{ 'db_file' } ) || croak( 'Could not open db file' );
187             binmode $fl, ':bytes';
188             seek $fl, $self->{db_begin} + $min * $self->{block_len}, 0;
189             read $fl, my $buf, $len * $self->{block_len};
190             close $fl;
191              
192             return $self->search_db( $buf, $ipn, 0, $len - 1 );
193             }
194              
195             sub search_idx {
196             my __PACKAGE__ $self = shift;
197             my $ipn = shift;
198             my $min = shift;
199             my $max = shift;
200              
201             my $offset;
202             while ( $max - $min > 8 ) {
203             $offset = ( $min + $max ) >> 1;
204              
205             if ( encode_utf8($ipn) gt encode_utf8( substr( ( $self->{m_idx_str} ), $offset * 4, 4 ) ) ) {
206             $min = $offset;
207             }
208             else {
209             $max = $offset;
210             }
211             }
212              
213             while ( encode_utf8($ipn) gt encode_utf8( substr( $self->{m_idx_str}, $min * 4, 4 ) ) && $min++ < $max ) {
214             }
215              
216             return $min;
217             }
218              
219             sub search_db {
220             my __PACKAGE__ $self = shift;
221             my $str = shift;
222             my $ipn = shift;
223             my $min = shift;
224             my $max = shift;
225              
226             if( $max - $min > 1 ) {
227             $ipn = substr( $ipn, 1 );
228             my $offset;
229             while ( $max - $min > 8 ){
230             $offset = ( $min + $max ) >> 1;
231              
232             if ( encode_utf8( $ipn ) gt encode_utf8( substr( $str, $offset * $self->{block_len}, 3 ) ) ) {
233             $min = $offset;
234             }
235             else {
236             $max = $offset;
237             }
238             }
239              
240             while ( encode_utf8( $ipn ) ge encode_utf8( substr( $str, $min * $self->{block_len}, 3 ) ) && $min++ < $max ){}
241             }
242             else {
243             return hex( bin2hex( substr( $str, $min * $self->{block_len} + 3 , 3 ) ) );
244             }
245              
246             return hex( bin2hex( substr( $str, $min * $self->{block_len} - $self->{id_len}, $self->{id_len} ) ) );
247             }
248              
249             sub bin2hex {
250             my $str = shift;
251              
252             my $res = '';
253             for my $i ( 0 .. length( $str ) - 1 ) {
254             $res .= sprintf( '%02s', sprintf( '%x', ord( substr( $str, $i, 1 ) ) ) );
255             }
256              
257             return $res;
258             }
259              
260             sub ip2long {
261             return unpack( 'l*', pack( 'l*', unpack( 'N*', inet_aton( shift ) ) ) );
262             }
263              
264             sub parse_info {
265             my __PACKAGE__ $self = shift;
266             my $seek = shift;
267              
268             my $info;
269              
270             if ( $seek < $self->{country_size} ) {
271             open( my $fl, $self->{db_file} ) || croak('Could not open db file');
272             binmode $fl, ':bytes';
273             seek $fl, $seek + $self->{cities_begin}, 0;
274             read $fl, my $buf, $self->{max_country};
275             close $fl;
276              
277             $info = extended_unpack( $self->{pack}[0], $buf );
278             }
279             else {
280             open( my $fl, $self->{db_file} ) || croak('Could not open db file');
281             binmode $fl, ':bytes';
282             seek $fl, $seek + $self->{cities_begin}, 0;
283             read $fl, my $buf, $self->{max_city};
284             close $fl;
285              
286             $info = extended_unpack( $self->{pack}[2], $buf );
287             }
288              
289             if ($info) {
290             return $info;
291             }
292             else {
293             return;
294             }
295             }
296              
297             sub extended_unpack {
298             my $flags = shift;
299             my $val = shift;
300              
301             my $pos = 0;
302             my $result = [];
303              
304             my @flags_arr = split '/', $flags;
305              
306             foreach my $flag_str ( @flags_arr ) {
307             my ( $type, $name ) = split ':', $flag_str;
308              
309             my $flag = substr $type, 0, 1;
310             my $num = substr $type, 1, 1;
311              
312             my $len;
313              
314             if ( $flag eq 't' ) {
315             }
316             elsif ( $flag eq 'T' ) {
317             $len = 1;
318             }
319             elsif ( $flag eq 's' ) {
320             }
321             elsif ( $flag eq 'n' ) {
322             $len = $num;
323             }
324             elsif ( $flag eq 'S' ) {
325             $len = 2;
326             }
327             elsif ( $flag eq 'm' ) {
328             }
329             elsif ( $flag eq 'M' ) {
330             $len = 3;
331             }
332             elsif ( $flag eq 'd' ) {
333             $len = 8;
334             }
335             elsif ( $flag eq 'c' ) {
336             $len = $num;
337             }
338             elsif ( $flag eq 'b' ) {
339             $len = index( $val, "\0", $pos ) - $pos;
340             }
341             else {
342             $len = 4;
343             }
344              
345             my $subval = substr( $val, $pos, $len );
346              
347             my $res;
348              
349             if ( $flag eq 't' ) {
350             $res = ( unpack 'c', $subval )[0];
351             }
352             elsif ( $flag eq 'T' ) {
353             $res = ( unpack 'C', $subval )[0];
354             }
355             elsif ( $flag eq 's' ) {
356             $res = ( unpack 's', $subval )[0];
357             }
358             elsif ( $flag eq 'S' ) {
359             $res = ( unpack 'S', $subval )[0];
360             }
361             elsif ( $flag eq 'm' ) {
362             $res = ( unpack 'l', $subval . ( ord( substr( $subval, 2, 1 ) ) >> 7 ? "\xff" : "\0" ) )[0];
363             }
364             elsif ( $flag eq 'M' ) {
365             $res = ( unpack 'L', $subval . "\0" )[0];
366             }
367             elsif ( $flag eq 'i' ) {
368             $res = ( unpack 'l', $subval )[0];
369             }
370             elsif ( $flag eq 'I' ) {
371             $res = ( unpack 'L', $subval )[0];
372             }
373             elsif ( $flag eq 'f' ) {
374             $res = ( unpack 'f', $subval )[0];
375             }
376             elsif ( $flag eq 'd' ) {
377             $res = ( unpack 'd', $subval )[0];
378             }
379             elsif ( $flag eq 'n' ) {
380             $res = ( unpack 's', $subval )[0] / ( 10 ** $num );
381             }
382             elsif ( $flag eq 'N' ) {
383             $res = ( unpack 'l', $subval )[0] / ( 10 ** $num );
384             }
385             elsif ( $flag eq 'c' ) {
386             $res = rtrim $subval;
387             }
388             elsif ( $flag eq 'b' ) {
389             $res = $subval;
390             $len++;
391             }
392              
393             $pos += $len;
394              
395             push @$result, $res;
396             }
397              
398             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