File Coverage

blib/lib/Net/Google/SafeBrowsing3.pm
Criterion Covered Total %
statement 134 517 25.9
branch 15 154 9.7
condition 4 87 4.6
subroutine 25 45 55.5
pod 24 24 100.0
total 202 827 24.4


line stmt bran cond sub pod time code
1             package Net::Google::SafeBrowsing3;
2              
3 1     1   53215 use strict;
  1         1  
  1         23  
4 1     1   5 use warnings;
  1         1  
  1         19  
5              
6 1     1   4 use Carp;
  1         1  
  1         46  
7 1     1   521 use LWP::UserAgent;
  1         38334  
  1         24  
8 1     1   6 use URI;
  1         2  
  1         21  
9 1     1   394 use Digest::SHA qw(sha256);
  1         2359  
  1         81  
10 1     1   6 use List::Util qw(first);
  1         2  
  1         46  
11 1     1   370 use Text::Trim;
  1         465  
  1         98  
12 1     1   357 use MIME::Base64::URLSafe;
  1         1111  
  1         45  
13 1     1   8 use MIME::Base64;
  1         2  
  1         31  
14 1     1   354 use String::HexConvert;
  1         262  
  1         46  
15 1     1   624 use IO::Socket::SSL 'inet4';
  1         65730  
  1         6  
16 1     1   656 use Google::ProtocolBuffers;
  1         86445  
  1         29  
17 1     1   6 use Data::Dumper;
  1         2  
  1         37  
18              
19 1     1   5 use Exporter 'import';
  1         1  
  1         50  
20             our @EXPORT = qw(DATABASE_RESET INTERNAL_ERROR SERVER_ERROR NO_UPDATE NO_DATA SUCCESSFUL MALWARE PHISHING UNWANTED LANDING DISTRIBUTION);
21              
22              
23             BEGIN {
24 1     1   4 IO::Socket::SSL::set_ctx_defaults(
25             # verify_mode => Net::SSLeay->VERIFY_PEER(),
26             SSL_verify_mode => 0,
27             );
28             }
29              
30             our $VERSION = '0.8';
31              
32             Google::ProtocolBuffers->parse("
33             message ChunkData {
34             required int32 chunk_number = 1;
35              
36             // The chunk type is either an add or sub chunk.
37             enum ChunkType {
38             ADD = 0;
39             SUB = 1;
40             }
41             optional ChunkType chunk_type = 2 [default = ADD];
42              
43             // Prefix type which currently is either 4B or 32B. The default is set
44             // to the prefix length, so it doesn't have to be set at all for most
45             // chunks.
46             enum PrefixType {
47             PREFIX_4B = 0;
48             FULL_32B = 1;
49             }
50             optional PrefixType prefix_type = 3 [default = PREFIX_4B];
51              
52             // Stores all SHA256 add or sub prefixes or full-length hashes. The number
53             // of hashes can be inferred from the length of the hashes string and the
54             // prefix type above.
55             optional bytes hashes = 4;
56              
57             // Sub chunks also encode one add chunk number for every hash stored above.
58             repeated int32 add_numbers = 5 [packed = true];
59              
60             }
61             ",
62             {create_accessors => 0 }
63             );
64              
65             Google::ProtocolBuffers->parse("
66             message MalwarePatternType {
67             enum PATTERN_TYPE {
68             LANDING = 1;
69             DISTRIBUTION = 2;
70             }
71              
72             required PATTERN_TYPE pattern_type = 1;
73             }
74             ",
75             {create_accessors => 0 }
76             );
77              
78             # TODO ###################################################
79             #Todo: request full hashes: seperate 32bytes for 4bytes
80             # Todo: optimize lookup_suffix, 1 search for all lists
81              
82             =head1 NAME
83              
84             This library is obsolete, please use Net::Google::SafeBrowsing4 instead.
85              
86             Net::Google::SafeBrowsing3 - Perl extension for the Google Safe Browsing v3 API. (Google Safe Browsing v2 has been deprecated by Google.)
87              
88             =head1 SYNOPSIS
89              
90             use Net::Google::SafeBrowsing3;
91             use Net::Google::SafeBrowsing3::Sqlite;
92            
93             my $storage = Net::Google::SafeBrowsing3::Sqlite->new(file => 'google-v3.db');
94             my $gsb = Net::Google::SafeBrowsing3->new(
95             key => "my key",
96             storage => $storage,
97             );
98            
99             $gsb->update();
100             my $match = $gsb->lookup(url => 'http://www.gumblar.cn/');
101            
102             if ($match eq MALWARE) {
103             print "http://www.gumblar.cn/ is flagged as a dangerous site\n";
104             }
105              
106             $storage->close();
107              
108             =head1 DESCRIPTION
109              
110             Net::Google::SafeBrowsing3 implements the Google Safe Browsing v3 API.
111              
112             The library passes most of the unit tests listed in the API documentation. See the documentation (L) for more details about the failed tests.
113              
114             The Google Safe Browsing database must be stored and managed locally. L uses Sqlite as the storage back-end, L uses MySQL. Other storage mechanisms (databases, memory, etc.) can be added and used transparently with this module.
115              
116             The source code is available on github at L.
117              
118             If you do not need to inspect more than 10,000 URLs a day, you can use L with the Google Safe Browsing v2 Lookup API which does not require to store and maintain a local database.
119              
120             IMPORTANT: If you start with an empty database, you will need to perform several updates to retrieve all the Google Safe Browsing information. This may require up to 24 hours. This is a limitation of the Google API, not of this module.
121              
122             IMPORTANT: Google Safe Browsing v3 requires a different key than v2.
123              
124              
125             =head1 CONSTANTS
126              
127             Several constants are exported by this module:
128              
129             =over 4
130              
131             =item DATABASE_RESET
132              
133             Google requested to reset (empty) the local database.
134              
135             =item INTERNAL_ERROR
136              
137             An internal error occurred.
138              
139             =item SERVER_ERROR
140              
141             The server sent an error back to the client.
142              
143             =item NO_UPDATE
144              
145             No update was performed, probably because it is too early to make a new request to Google Safe Browsing.
146              
147             =item NO_DATA
148              
149             No data was sent back by Google to the client, probably because the database is up to date.
150              
151             =item SUCCESSFUL
152              
153             The operation was successful.
154              
155             =item MALWARE
156              
157             Name of the Malware list in Google Safe Browsing (shortcut to 'goog-malware-shavar')
158              
159             =item PHISHING
160              
161             Name of the Phishing list in Google Safe Browsing (shortcut to 'googpub-phish-shavar')
162              
163             =item UNWANTED
164              
165             Name of the Unwamted Application list in Google Safe Browsing (shortcut to 'goog-unwanted-shavar')
166              
167             =item LANDING
168              
169             Landing site.
170              
171             =item DISTRIBUTION
172              
173             Distribution site.
174              
175             =back
176              
177             =cut
178              
179             use constant {
180 1         161 DATABASE_RESET => -6,
181             INTERNAL_ERROR => -3, # internal/parsing error
182             SERVER_ERROR => -2, # Server sent an error back
183             NO_UPDATE => -1, # no update (too early)
184             NO_DATA => 0, # no data sent
185             SUCCESSFUL => 1, # data sent
186             MALWARE => 'goog-malware-shavar',
187             PHISHING => 'googpub-phish-shavar',
188             UNWANTED => 'goog-unwanted-shavar',
189             LANDING => 1, # Metadata goog-malware-shavar
190             DISTRIBUTION => 2, # Metadata goog-malware-shavar
191 1     1   142 };
  1         3  
192              
193              
194             =head1 CONSTRUCTOR
195              
196             =over 4
197              
198             =back
199              
200             =head2 new()
201              
202             Create a Net::Google::SafeBrowsing3 object
203              
204             my $gsb = Net::Google::SafeBrowsing3->new(
205             key => "my key",
206             storage => Net::Google::SafeBrowsing3::Sqlite->new(file => 'google-v3.db'),
207             debug => 0,
208             list => MALWARE,
209             );
210              
211             Arguments
212              
213             =over 4
214              
215             =item server
216              
217             Safe Browsing Server. https://safebrowsing.google.com/safebrowsing/ by default
218              
219             =item key
220              
221             Required. Your Google Safe browsing API key
222              
223             =item storage
224              
225             Required. Object which handle the storage for the Google Safe Browsing database. See L for more details.
226              
227             =item list
228              
229             Optional. The Google Safe Browsing list to handle. By default, handles both MALWARE and PHISHING.
230              
231             =item debug
232              
233             Optional. Set to 1 to enable debugging. 0 (disabled) by default.
234              
235             The debug output maybe quite large and can slow down significantly the update and lookup functions.
236              
237             =item errors
238              
239             Optional. Set to 1 to show errors to STDOUT. 0 (disabled by default).
240              
241             =item perf
242              
243             Optional. Set to 1 to show performance information.
244              
245             =item version
246              
247             Optional. Google Safe Browsing version. 3.0 by default
248              
249             =back
250              
251             =cut
252              
253             sub new {
254 1     1 1 511 my ($class, %args) = @_;
255              
256 1         10 my $self = { # default arguments
257             server => 'https://safebrowsing.google.com/safebrowsing/',
258             list => [PHISHING, MALWARE, UNWANTED],
259             key => '',
260             version => '3.0',
261             debug => 0,
262             errors => 0,
263             last_error => '',
264             perf => 0,
265              
266             %args,
267             };
268              
269 1 50       5 if (! exists $self->{storage}) {
270 1     1   408 use Net::Google::SafeBrowsing3::Storage;
  1         2  
  1         5222  
271 1         6 $self->{storage} = Net::Google::SafeBrowsing3::Storage->new();
272             }
273 1 50       5 if (ref $self->{list} ne 'ARRAY') {
274 0         0 $self->{list} = [$self->{list}];
275             }
276              
277 1 50       5 bless $self, $class or croak "Can't bless $class: $!";
278 1         3 return $self;
279             }
280              
281             =head1 PUBLIC FUNCTIONS
282              
283             =over 4
284              
285             =back
286              
287             =head2 update()
288              
289             Perform a database update.
290              
291             $gsb->update();
292              
293             Return the status of the update (see the list of constants above): INTERNAL_ERROR, SERVER_ERROR, NO_UPDATE, NO_DATA or SUCCESSFUL
294              
295             This function can handle two lists at the same time. If one of the list should not be updated, it will automatically skip it and update the other one. It is faster to update two lists at once rather than doing them one by one.
296              
297              
298             Arguments
299              
300             =over 4
301              
302             =item list
303              
304             Optional. Update a specific list. Use the list(s) from new() by default.
305              
306              
307             =item force
308              
309             Optional. Force the update (1). Disabled by default (0).
310              
311             Be careful if you set this option to 1 as too frequent updates might result in the blacklisting of your API key.
312              
313             =back
314              
315             =cut
316              
317             sub update {
318 0     0 1 0 my ($self, %args) = @_;
319 0         0 my $list = $args{list};
320 0   0     0 my $force = $args{force} || 0;
321              
322 0         0 my @lists = @{$self->{list}};
  0         0  
323 0 0       0 @lists = @{[$args{list}]} if (defined $list);
  0         0  
324              
325 0         0 my $result = 0;
326              
327             # Too early to update?
328 0         0 my $start = time();
329 0         0 my $i = 0;
330 0         0 while ($i < scalar @lists) {
331 0         0 my $list = $lists[$i];
332 0         0 my $info = $self->{storage}->last_update(list => $list);
333            
334 0 0 0     0 if ($info->{'time'} + $info->{'wait'} > time && $force == 0) {
335 0         0 $self->debug("Too early to update $list\n");
336 0         0 splice(@lists, $i, 1);
337             }
338             else {
339 0         0 $self->debug("OK to update $list: " . time() . "/" . ($info->{'time'} + $info->{'wait'}) . "\n");
340 0         0 $i++;
341             }
342             }
343              
344 0 0       0 if (scalar @lists == 0) {
345 0         0 $self->debug("Too early to update any list\n");
346 0         0 return NO_UPDATE;
347             }
348 0         0 $self->perf("OK to update check: " . (time() - $start) . "s\n");
349            
350              
351 0         0 my $ua = $self->ua;
352              
353 0         0 my $url = $self->{server} . "downloads?client=api&key=" . $self->{key} . "&appver=$VERSION&pver=" . $self->{version};
354              
355 0         0 my $body = '';
356 0         0 foreach my $list (@lists) {
357             # Report existng chunks
358 0         0 $start = time();
359 0         0 my $a_range = $self->create_range(numbers => [$self->{storage}->get_add_chunks_nums(list => $list)]);
360 0         0 my $s_range = $self->create_range(numbers => [$self->{storage}->get_sub_chunks_nums(list => $list)]);
361 0         0 $self->perf("Create add and sub ranges: " . (time() - $start) . "s\n");
362            
363 0         0 my $chunks_list = '';
364 0 0       0 if ($a_range ne '') {
365 0         0 $chunks_list .= "a:$a_range";
366             }
367 0 0       0 if ($s_range ne '') {
368 0 0       0 $chunks_list .= ":" if ($a_range ne '');
369 0         0 $chunks_list .= "s:$s_range";
370             }
371              
372 0         0 $body .= "$list;$chunks_list";
373 0         0 $body .= "\n";
374             }
375              
376 0         0 my $start_req = time();
377 0         0 my $res = $ua->post($url, Content => $body);
378 0         0 $self->perf("$body\n");
379              
380 0         0 $self->debug($res->request->as_string, "\n", $res->as_string . "\n");
381 0         0 my $duration_req = time() - $start_req;
382              
383 0 0       0 if (! $res->is_success) {
384 0         0 $self->error("Request failed\n");
385              
386 0         0 foreach my $list (@lists) {
387 0         0 $self->update_error('time' => time(), list => $list);
388             }
389              
390 0         0 return SERVER_ERROR;
391             }
392              
393 0         0 my $last_update = time;
394 0         0 my $wait = 0;
395              
396 0         0 my @redirections = ();
397 0         0 my $del_add_duration = 0;
398 0         0 my $del_sub_duration = 0;
399 0         0 my $add_range_info = '';
400              
401             # API doc: Clients must clear cached full-length hashes each time they send an update request.
402 0         0 foreach my $list (@lists) {
403 0         0 $self->{storage}->reset_full_hashes(list => $list);
404             }
405              
406 0         0 my @lines = split/\s/, $res->decoded_content;
407 0         0 $list = '';
408 0         0 foreach my $line (@lines) {
409 0 0       0 if ($line =~ /n:\s*(\d+)\s*$/) {
    0          
    0          
    0          
    0          
    0          
    0          
410 0         0 $self->debug("Next poll: $1 seconds\n");
411 0         0 $wait = $1;
412             }
413             elsif ($line =~ /i:\s*(\S+)\s*$/) {
414 0         0 $self->debug("List: $1\n");
415 0         0 $list = $1;
416             }
417             elsif ($line =~ /u:\s*(\S+),(\S+)\s*$/) {
418 0         0 $self->debug("Redirection: $1\n");
419 0         0 $self->debug("MAC: $2\n");
420 0         0 push(@redirections, [$1, $list, $2]);
421             }
422             elsif ($line =~ /u:\s*(\S+)\s*$/) {
423 0         0 $self->debug("Redirection: $1\n");
424 0         0 push(@redirections, [$1, $list, '']);
425             }
426             elsif ($line =~ /ad:(\S+)$/) {
427 0         0 $self->debug("Delete Add Chunks: $1\n");
428              
429 0         0 my $del_add_start = time();
430 0         0 $add_range_info = $1 . " $list";
431 0         0 my @nums = $self->expand_range(range => $1);
432 0         0 $self->{storage}->delete_add_ckunks(chunknums => [@nums], list => $list);
433 0         0 $del_add_duration = time() - $del_add_start;
434              
435 0         0 $result = 1;
436             }
437             elsif ($line =~ /sd:(\S+)$/) {
438 0         0 $self->debug("Delete Sub Chunks: $1\n");
439              
440 0         0 my $del_sub_start = time();
441 0         0 my @nums = $self->expand_range(range => $1);
442 0         0 $self->{storage}->delete_sub_ckunks(chunknums => [@nums], list => $list);
443 0         0 $del_sub_duration = time() - $del_sub_start;
444              
445 0         0 $result = 1;
446             }
447             elsif ($line =~ /r:pleasereset/) {
448 0         0 $self->debug("Database must be reset\n");
449              
450 0         0 $self->{storage}->reset(list => $list);
451              
452 0         0 return DATABASE_RESET;
453             }
454             }
455 0         0 $self->debug("\n");
456 0         0 $self->perf("Handle first request: " . (time() - $last_update) . "s (POST: ${duration_req}s, DEL add: ${del_add_duration}s, DEL sub: ${del_sub_duration}s, ADD range: ${add_range_info})\n");
457              
458 0 0       0 $result = 1 if (scalar @redirections > 0);
459              
460 0         0 $self->perf("Parse redirections: ");
461 0         0 foreach my $data (@redirections) {
462 0         0 $start = time();
463 0         0 my $redirection = $data->[0];
464 0         0 $list = $data->[1];
465              
466 0         0 $self->debug("Checking redirection https://$redirection ($list)\n");
467 0         0 $res = $ua->get("https://$redirection");
468 0 0       0 if (! $res->is_success) {
469 0         0 $self->error("Request to $redirection failed\n");
470              
471 0         0 foreach my $list (@lists) {
472 0         0 $self->update_error('time' => $last_update, list => $list);
473             }
474              
475 0         0 return SERVER_ERROR;
476             }
477            
478 0 0       0 $self->debug(substr($res->as_string, 0, 250) . "\n\n") if ($self->{debug});
479 0 0       0 $self->debug(substr($res->content, 0, 250) . "\n\n") if ($self->{debug});
480            
481 0         0 my $data = $res->content;
482              
483 0         0 my $result = $self->parse_data(data => $data, list => $list);
484 0 0       0 if ($result != SUCCESSFUL) {
485 0         0 foreach my $list (@lists) {
486 0         0 $self->update_error('time' => $last_update, list => $list);
487             }
488              
489 0         0 return $result;
490             }
491 0         0 $self->perf((time() - $start) . "s ");
492             }
493 0         0 $self->perf("\n");
494              
495 0         0 foreach my $list (@lists) {
496 0         0 $self->debug("List update: $last_update $wait $list\n");
497 0         0 $self->{storage}->updated('time' => $last_update, 'wait' => $wait, list => $list);
498             }
499              
500 0         0 return $result; # ok
501             }
502              
503             =head2 lookup()
504              
505             Lookup a URL against the Google Safe Browsing database.
506              
507             my $match = $gsb->lookup(url => 'http://www.gumblar.cn');
508             my ($match, $type) = $gsb->lookup(url => 'http://www.gumblar.cn');
509              
510             In scalar context, returns the name of the list if there is any match, returns an empty string otherwise.
511             In array context, return the name of the list (empty if no match) and the type of malware site (0 if no type specified)
512              
513             Arguments
514              
515             =over 4
516              
517             =item list
518              
519             Optional. Lookup against a specific list. Use the list(s) from new() by default.
520              
521             =item url
522              
523             Required. URL to lookup.
524              
525             =back
526              
527             =cut
528              
529             sub lookup {
530 0     0 1 0 my ($self, %args) = @_;
531 0   0     0 my $list = $args{list} || '';
532 0   0     0 my $url = $args{url} || return '';
533              
534 0         0 my @lists = @{$self->{list}};
  0         0  
535 0 0       0 @lists = @{[$args{list}]} if ($list ne '');
  0         0  
536              
537              
538             # TODO: create our own URI management for canonicalization
539             # fix for http:///foo.com (3 ///)
540 0         0 $url =~ s/^(https?:\/\/)\/+/$1/;
541              
542              
543 0         0 my $uri = URI->new($url)->canonical;
544 0         0 my ($match, $type) = $self->lookup_suffix(lists => [@lists], url => $url);
545 0 0       0 return ($match, $type) if (wantarray);
546 0         0 return $match
547             }
548              
549              
550              
551              
552              
553              
554              
555              
556             =pod
557              
558             =head1 PRIVATE FUNCTIONS
559              
560             These functions are not intended to be used externally.
561              
562             =over 4
563              
564             =back
565              
566             =head2 lookup_suffix()
567              
568             Lookup a host prefix.
569              
570             =cut
571              
572             sub lookup_suffix {
573 0     0 1 0 my ($self, %args) = @_;
574 0   0     0 my $lists = $args{lists} || croak "Missing lists\n";
575 0   0     0 my $url = $args{url} || return '';
576              
577             # Calculate prefixes
578 0         0 my @full_hashes = $self->full_hashes($url);
579 0         0 my @full_hashes_prefix = map (substr($_, 0, 4), @full_hashes); # Get the prefixes from the first 4 bytes
580              
581             # Local lookup
582 0         0 my @add_chunks = $self->local_lookup_suffix(lists => $lists, url => $url, full_hashes => [@full_hashes], full_hashes_prefix => [@full_hashes_prefix]);
583 0 0       0 if (scalar @add_chunks == 0) {
584 0         0 $self->debug("No hit in local lookup\n");
585 0 0       0 return ('', 0) if (wantarray);
586 0         0 return '';
587             }
588              
589 0         0 $self->debug("Found ", scalar(@add_chunks), " add chunk(s) in local database\n");
590 0         0 foreach my $add (@add_chunks) {
591 0         0 $self->debug("Chunk: ", $self->hex_to_ascii($add->{prefix}), " - ", $add->{list}, "\n");
592             }
593              
594             # get stored full hashes
595 0         0 foreach my $hash (@full_hashes) {
596 0         0 foreach my $list (@$lists) {
597 0         0 my @hashes = $self->{storage}->get_full_hashes(hash => $hash, list => $list);
598            
599 0 0       0 if (scalar @hashes > 0) {
600 0         0 $self->debug("Full hashes found: ", scalar(@hashes), "\n");
601 0         0 my $result = pop(@hashes);
602              
603 0 0 0     0 return ($list, $result->{type} || 0) if (wantarray);
604 0         0 return $list;
605             }
606             }
607             }
608              
609              
610             # ask for new hashes
611             # TODO: make sure we don't keep asking for the same over and over
612 0         0 my @hashes = $self->request_full_hash(prefixes => [ map($_->{prefix}, @add_chunks) ]);
613 0         0 $self->{storage}->add_full_hashes(full_hashes => [@hashes], timestamp => time());
614              
615 0         0 foreach my $full_hash (@full_hashes) {
616 0     0   0 my $hash = first { $_->{hash} eq $full_hash} @hashes;
  0         0  
617 0 0       0 next if (! defined $hash);
618              
619 0     0   0 my $list = first { $hash->{list} eq $_ } @$lists;
  0         0  
620              
621 0 0 0     0 if (defined $hash && defined $list) {
622             # $self->debug($self->hex_to_ascii($hash->{hash}) . " eq " . $self->hex_to_ascii($full_hash) . "\n\n");
623              
624 0         0 $self->debug("Match: " . $self->hex_to_ascii($full_hash) . "\n");
625              
626 0 0 0     0 return ($hash->{list}, $hash->{type} || 0) if (wantarray);
627 0         0 return $hash->{list};
628             }
629             # elsif (defined $hash) {
630             # $self->debug("hash: " . $self->hex_to_ascii($hash->{hash}) . "\n");
631             # $self->debug("list: " . $hash->{list} . "\n");
632             # }
633             }
634            
635 0         0 $self->debug("No match\n");
636 0 0       0 return ('', 0) if (wantarray);
637 0         0 return '';
638             }
639              
640             =head2 local_lookup_suffix()
641              
642             Lookup a host prefix in the local database only.
643              
644             =cut
645             sub local_lookup_suffix {
646 0     0 1 0 my ($self, %args) = @_;
647 0   0     0 my $lists = $args{lists} || croak "Missing lists\n";
648 0   0     0 my $url = $args{url} || return ();
649 0   0     0 my $full_hashe_list = $args{full_hashes} || [];
650 0   0     0 my $full_hashes_prefix_list = $args{full_hashes_prefix} || [];
651              
652              
653              
654             # Step 1: calculate prefixes if not provided
655             # Get the prefixes from the first 4 bytes
656 0         0 my @full_hashes = @{$full_hashe_list};
  0         0  
657 0         0 my @full_hashes_prefix = @{$full_hashes_prefix_list};
  0         0  
658 0 0       0 if (scalar @full_hashes_prefix == 0) {
659 0 0       0 @full_hashes = $self->full_hashes($url) if (scalar @full_hashes == 0);
660              
661 0         0 @full_hashes_prefix = map (substr($_, 0, 4), @full_hashes);
662             }
663              
664             # Step 2: get all add chunks for these suffixes
665             # Do it for all lists
666 0         0 my @add_chunks = ();
667 0         0 foreach my $prefix (@full_hashes_prefix, @full_hashes) {
668 0         0 push(@add_chunks, $self->{storage}->get_add_chunks(prefix => $prefix));
669             }
670              
671 0 0       0 if (scalar @add_chunks == 0) { # no match
672 0         0 $self->debug("No prefix found\n");
673 0         0 return @add_chunks;
674             }
675              
676              
677             # Step 3: get all sub chunks for this host key
678 0         0 my @sub_chunks = ();
679 0         0 foreach my $prefix (@full_hashes_prefix, @full_hashes) {
680 0         0 push(@sub_chunks, $self->{storage}->get_sub_chunks(hostkey => $prefix));
681             }
682              
683 0         0 foreach my $sub_chunk (@sub_chunks) {
684 0         0 my $i = 0;
685 0         0 while ($i < scalar @add_chunks) {
686 0         0 my $add_chunk = $add_chunks[$i];
687              
688 0 0 0     0 if ($add_chunk->{chunknum} != $sub_chunk->{addchunknum} || $add_chunk->{list} ne $sub_chunk->{list}) {
689 0         0 $i++;
690 0         0 next;
691             }
692              
693 0 0       0 if ($sub_chunk->{prefix} eq $add_chunk->{prefix}) {
694 0         0 splice(@add_chunks, $i, 1);
695             }
696             else {
697 0         0 $i++;
698             }
699             }
700             }
701              
702 0 0       0 if (scalar @add_chunks == 0) {
703 0         0 $self->debug("All add_chunks have been removed by sub_chunks\n");
704             }
705              
706 0         0 return @add_chunks;
707             }
708              
709              
710             =head2 update_error()
711              
712             Handle server errors during a database update.
713              
714             =cut
715              
716             sub update_error {
717 0     0 1 0 my ($self, %args) = @_;
718 0   0     0 my $time = $args{'time'} || time;
719 0   0     0 my $list = $args{'list'} || '';
720              
721 0         0 my $info = $self->{storage}->last_update(list => $list);
722 0 0       0 $info->{errors} = 0 if (! exists $info->{errors});
723 0         0 my $errors = $info->{errors} + 1;
724 0         0 my $wait = 0;
725              
726 0 0       0 $wait = $errors == 1 ? 60
    0          
    0          
    0          
    0          
    0          
727             : $errors == 2 ? int(30 * 60 * (rand(1) + 1)) # 30-60 mins
728             : $errors == 3 ? int(60 * 60 * (rand(1) + 1)) # 60-120 mins
729             : $errors == 4 ? int(2 * 60 * 60 * (rand(1) + 1)) # 120-240 mins
730             : $errors == 5 ? int(4 * 60 * 60 * (rand(1) + 1)) # 240-480 mins
731             : $errors > 5 ? 480 * 60
732             : 0;
733              
734 0         0 $self->{storage}->update_error('time' => $time, list => $list, 'wait' => $wait, errors => $errors);
735              
736             }
737              
738              
739             =head2 ua()
740              
741             Create LWP::UserAgent to make HTTP requests to Google.
742              
743             =cut
744              
745             sub ua {
746 0     0 1 0 my ($self, %args) = @_;
747              
748 0 0       0 if (! exists $self->{ua}) {
749 0         0 my $ua = LWP::UserAgent->new;
750 0         0 $ua->timeout(60);
751              
752 0         0 $self->{ua} = $ua;
753             }
754              
755 0         0 return $self->{ua};
756             }
757              
758              
759             =head2 parse_data()
760              
761             Parse data from a redirection (add and sub chunk information).
762              
763             =cut
764              
765             sub parse_data {
766 0     0 1 0 my ($self, %args) = @_;
767 0   0     0 my $data = $args{data} || '';
768 0   0     0 my $list = $args{list} || '';
769              
770 0         0 my $chunk_num = 0;
771 0         0 my $hash_length = 0;
772 0         0 my $chunk_length = 0;
773              
774 0         0 while (length $data > 0) {
775             # my $length = substr($data, 0, 4); # HEX
776 0         0 my $length = hex $self->hex_to_ascii( substr($data, 0, 4, '') );
777 0         0 $self->debug("Length: $length\n");
778 0         0 my $chunk = substr($data, 0, $length, '');
779 0         0 my $data = ChunkData->decode($chunk);
780 0         0 $self->debug(Dumper($data), "\n");
781              
782 0 0 0     0 if (! exists($data->{chunk_type}) || $data->{chunk_type} == 0) {
783 0         0 my @chunks = $self->parse_a(chunk => $data);
784 0         0 $self->{storage}->add_chunks(type => 'a', chunknum => $data->{chunk_number}, chunks => [@chunks], list => $list);
785             }
786             else {
787 0         0 my @chunks = $self->parse_s(chunk => $data);
788 0         0 $self->{storage}->add_chunks(type => 's', chunknum => $data->{chunk_number}, chunks => [@chunks], list => $list);
789             }
790             }
791              
792 0         0 return SUCCESSFUL;
793             }
794              
795             =head2 parse_s()
796              
797             Parse s chunks information for a database update.
798              
799             =cut
800              
801             sub parse_s {
802 0     0 1 0 my ($self, %args) = @_;
803 0   0     0 my $chunk = $args{chunk} || return ();
804              
805             # {
806             # 'add_numbers' => [
807             # 161383,
808             # 156609,
809             # 161686,
810             # 159174,
811             # 166040,
812             # 164187
813             # ],
814             # 'chunk_type' => 1,
815             # 'chunk_number' => 158095,
816             # 'hashes' => ' _*���F�E����A��;;v����i'
817             # }
818              
819 0         0 my @data = ();
820 0   0     0 my $prefix_type = $chunk->{prefix_type} || 0;
821 0   0     0 my $prefix = $chunk->{hashes} || ''; # HEX
822 0         0 $self->debug("Hashes length: ", length($prefix), "\n");
823 0 0       0 $self->debug("Hashes: ", $self->hex_to_ascii($prefix), "\n") if ($self->{debug});
824              
825 0         0 my $hash_length = 4;
826 0 0       0 $hash_length = 32 if ($prefix_type == 1);
827 0         0 my @hashes = ();
828 0         0 while(length($prefix) > 0) {
829 0         0 push(@hashes, substr($prefix, 0, $hash_length, ''));
830             }
831              
832 0         0 for(my $i = 0; $i < scalar @{ $chunk->{add_numbers} }; $i++) {
  0         0  
833 0         0 push(@data, { add_chunknum => ${ $chunk->{add_numbers} }[$i], prefix => $hashes[$i] });
  0         0  
834             }
835              
836 0         0 return @data;
837             }
838              
839              
840             =head2 parse_a()
841              
842             Parse a chunks information for a database update.
843              
844             =cut
845              
846             sub parse_a {
847 0     0 1 0 my ($self, %args) = @_;
848 0   0     0 my $chunk = $args{chunk} || return ();
849              
850             # {
851             # 'chunk_number' => 166146,
852             # 'hashes' => 'Z[�$~�����w5���B�;0����z;�E&�ʳY�H$`-'
853             # }
854              
855 0         0 my @data = ();
856 0   0     0 my $prefix_type = $chunk->{prefix_type} || 0;
857 0   0     0 my $prefix = $chunk->{hashes} || ''; # HEX
858              
859 0         0 my $hash_length = 4;
860 0 0       0 $hash_length = 32 if ($prefix_type == 1);
861              
862 0         0 while(length($prefix) > 0) {
863 0         0 push(@data, { prefix => substr($prefix, 0, $hash_length, '') });
864             }
865              
866 0         0 return @data;
867             }
868              
869             =head2 hex_to_ascii()
870              
871             Transform hexadecimal strings to printable ASCII strings. Used mainly for debugging.
872              
873             print $gsb->hex_to_ascii('hex value');
874              
875             =cut
876              
877             sub hex_to_ascii {
878 2     2 1 7 my ($self, $hex) = @_;
879              
880 2         5 return String::HexConvert::ascii_to_hex($hex);
881             }
882              
883              
884             =head2 ascii_to_hex()
885              
886             Transform ASCII strings to hexadecimal strings.
887              
888             =cut
889              
890             sub ascii_to_hex {
891 1     1 1 462 my ($self, $ascii) = @_;
892              
893 1         3 my $hex = '';
894 1         6 for (my $i = 0; $i < int(length($ascii) / 2); $i++) {
895 4         12 $hex .= chr hex( substr($ascii, $i * 2, 2) );
896             }
897              
898 1         4 return $hex;
899             }
900              
901             =head2 debug()
902              
903             Print debug output.
904              
905             =cut
906              
907             sub debug {
908 0     0 1 0 my ($self, @messages) = @_;
909              
910 0 0       0 print join('', @messages) if ($self->{debug} > 0);
911             }
912              
913              
914             =head2 error()
915              
916             Print error message.
917              
918             =cut
919              
920             sub error {
921 0     0 1 0 my ($self, $message) = @_;
922              
923 0 0 0     0 print "ERROR - ", $message if ($self->{debug} > 0 || $self->{errors} > 0);
924 0         0 $self->{last_error} = $message;
925             }
926              
927              
928             =head2 perf()
929              
930             Print performance message.
931              
932             =cut
933              
934             sub perf {
935 0     0 1 0 my ($self, @messages) = @_;
936              
937 0 0       0 print join('', @messages)if ($self->{perf} > 0);
938             }
939              
940              
941             =head2 canonical_domain()
942              
943             Find all canonical domains a domain.
944              
945             =cut
946              
947             sub canonical_domain {
948 11     11 1 4978 my ($self, $domain) = @_;
949              
950             # Remove all leading and trailing dots.
951 11         24 $domain =~ s/^\.+//;
952 11         31 $domain =~ s/\.+$//;
953              
954             # Replace consecutive dots with a single dot.
955 11         33 while ($domain =~ s/\.\.+/\./g) { }
956              
957             # Lowercase the whole string.
958 11         18 $domain = lc $domain;
959              
960 11         22 my @domains = ($domain);
961              
962              
963 11 100       24 if ($domain =~ /^\d+\.\d+\.\d+\.\d+$/) { # loose check for IP address, should be enough
964 1         3 return @domains;
965             }
966              
967 10         24 my @parts = split/\./, $domain;
968 10         18 splice(@parts, 0, -6); # take 5 top most compoments
969              
970              
971 10         19 while (scalar @parts > 2) {
972 9         11 shift @parts;
973 9         21 push(@domains, join(".", @parts) );
974             }
975              
976 10         32 return @domains;
977             }
978              
979             =head2 canonical_path()
980              
981             Find all canonical paths for a URL.
982              
983             =cut
984              
985             sub canonical_path {
986 4     4 1 38 my ($self, $path) = @_;
987              
988 4         7 my @paths = ($path); # return full path
989            
990             # without query string
991 4 100       8 if ($path =~ /\?/) {
992 1         4 $path =~ s/\?.*$//;
993              
994 1         2 push(@paths, $path);
995             }
996              
997 4         9 my @parts = split /\//, $path;
998 4 50       18 if (scalar @parts > 4) {
999 0         0 @parts = splice(@parts, -4, 4);
1000             }
1001              
1002             # if (scalar @parts == 0) {
1003             # push(@paths, "/");
1004             # }
1005              
1006              
1007 4         8 my $previous = '';
1008 4         8 while (scalar @parts > 1) {
1009 4         7 my $val = shift(@parts);
1010 4         6 $previous .= "$val/";
1011              
1012 4         10 push(@paths, $previous);
1013             }
1014            
1015 4         9 return @paths;
1016             }
1017              
1018             =head2 canonical()
1019              
1020             Find all canonical URLs for a URL.
1021              
1022             =cut
1023              
1024             sub canonical {
1025 4     4 1 9934 my ($self, $url) = @_;
1026              
1027 4         8 my @urls = ();
1028              
1029             # my $uri = URI->new($url)->canonical;
1030 4         8 my $uri = $self->canonical_uri($url);
1031 4         188 my @domains = $self->canonical_domain($uri->host);
1032 4         13 my @paths = $self->canonical_path($uri->path_query);
1033              
1034 4         8 foreach my $domain (@domains) {
1035 10         11 foreach my $path (@paths) {
1036 22         37 push(@urls, "$domain$path");
1037             }
1038             }
1039              
1040 4         17 return @urls;
1041             }
1042              
1043              
1044             =head2 canonical_uri()
1045              
1046             Create a canonical URI.
1047              
1048             NOTE: URI cannot handle all the test cases provided by Google. This method is a hack to pass most of the test. A few tests are still failing. The proper way to handle URL canonicalization according to Google would be to create a new module to handle URLs. However, I believe most real-life cases are handled correctly by this function.
1049              
1050             =cut
1051              
1052             sub canonical_uri {
1053 40     40 1 16816 my ($self, $url) = @_;
1054              
1055 40         93 $url = trim $url;
1056              
1057             # Special case for \t \r \n
1058 40         805 while ($url =~ s/^([^?]+)[\r\t\n]/$1/sgi) { }
1059              
1060 40         128 my $uri = URI->new($url)->canonical; # does not deal with directory traversing
1061              
1062             # $self->debug("0. $url => " . $uri->as_string . "\n");
1063              
1064            
1065 40 100 66     11673 if (! $uri->scheme() || $uri->scheme() eq '') {
1066 3         28 $uri = URI->new("http://$url")->canonical;
1067             }
1068              
1069 40         1321 $uri->fragment('');
1070              
1071 40         547 my $escape = $uri->as_string;
1072              
1073             # Reduce double // to single / in path
1074 40         247 while ($escape =~ s/^([a-z]+:\/\/[^?]+)\/\//$1\//sgi) { }
1075              
1076              
1077             # Remove empty fragment
1078 40         121 $escape =~ s/#$//;
1079              
1080             # canonial does not handle ../
1081             # $self->debug("\t$escape\n");
1082 40         113 while($escape =~ s/([^\/])\/([^\/]+)\/\.\.([\/?].*)$/$1$3/gi) { }
1083 40         86 while($escape =~ s/([^\/])\/([^\/]+)\/\.\.$/$1/gi) { }
1084              
1085             # May have removed ending /
1086             # $self->debug("\t$escape\n");
1087 40 100       110 $escape .= "/" if ($escape =~ /^[a-z]+:\/\/[^\/\?]+$/);
1088 40         83 $escape =~ s/^([a-z]+:\/\/[^\/]+)(\?.*)$/$1\/$2/gi;
1089             # $self->debug("\t$escape\n");
1090              
1091             # other weird case if domain = digits only, try to translate it to IP address
1092 40 100       86 if ((my $domain = URI->new($escape)->host) =~/^\d+$/) {
1093 3         217 my $ip = Socket::inet_ntoa(Socket::inet_aton($domain));
1094              
1095 3         9 $uri = URI->new($escape);
1096 3         164 $uri->host($ip);
1097              
1098 3         191 $escape = $uri->as_string;
1099             }
1100              
1101             # $self->debug("1. $url => $escape\n");
1102              
1103             # Try to escape the path again
1104 40         2526 $url = $escape;
1105 40         71 while (($escape = URI::Escape::uri_unescape($url)) ne $escape) { # wrong for %23 -> #
1106 0         0 $url = $escape;
1107             }
1108             # while (($escape = URI->new($url)->canonical->as_string) ne $escape) { # breask more unit tests than previous
1109             # $url = $escape;
1110             # }
1111              
1112             # Fix for %23 -> #
1113 40         329 while($escape =~ s/#/%23/sgi) { }
1114              
1115             # $self->debug("2. $url => $escape\n");
1116              
1117             # Fix over escaping
1118 40         78 while($escape =~ s/^([^?]+)%%(%.*)$/$1%25%25$2/sgi) { }
1119 40         61 while($escape =~ s/^([^?]+)%%/$1%25%25/sgi) { }
1120              
1121             # URI has issues with % in domains, it gets the host wrong
1122              
1123             # 1. fix the host
1124             # $self->debug("Domain: " . URI->new($escape)->host . "\n");
1125 40         66 my $exception = 0;
1126 40         133 while ($escape =~ /^[a-z]+:\/\/[^\/]*([^a-z0-9%_.-\/:])[^\/]*(\/.*)$/) {
1127 3         6 my $source = $1;
1128 3         10 my $target = sprintf("%02x", ord($source));
1129              
1130 3         45 $escape =~ s/^([a-z]+:\/\/[^\/]*)\Q$source\E/$1%\Q$target\E/;
1131              
1132 3         16 $exception = 1;
1133             }
1134              
1135             # 2. need to parse the path again
1136 40 50 66     85 if ($exception && $escape =~ /^[a-z]+:\/\/[^\/]+\/(.+)/) {
1137 0         0 my $source = $1;
1138 0         0 my $target = URI::Escape::uri_unescape($source);
1139              
1140             # print "Source: $source\n";
1141 0         0 while ($target ne URI::Escape::uri_unescape($target)) {
1142 0         0 $target = URI::Escape::uri_unescape($target);
1143             }
1144              
1145            
1146 0         0 $escape =~ s/\/\Q$source\E/\/$target/;
1147              
1148 0         0 while ($escape =~ s/#/%23/sgi) { } # fragement has been removed earlier
1149 0         0 while ($escape =~ s/^([a-z]+:\/\/[^\/]+\/.*)%5e/$1\&/sgi) { } # not in the host name
1150             # while ($escape =~ s/%5e/&/sgi) { }
1151              
1152 0         0 while ($escape =~ s/%([^0-9a-f]|.[^0-9a-f])/%25$1/sgi) { }
1153             }
1154              
1155             # $self->debug("$url => $escape\n");
1156             # $self->debug(URI->new($escape)->as_string . "\n");
1157              
1158 40         82 return URI->new($escape);
1159             }
1160              
1161             =head2 full_hashes()
1162              
1163             Return all possible full hashes for a URL.
1164              
1165             =cut
1166              
1167             sub full_hashes {
1168 0     0 1   my ($self, $url) = @_;
1169              
1170 0           my @urls = $self->canonical($url);
1171 0           my @hashes = ();
1172              
1173 0           foreach my $url (@urls) {
1174             # $self->debug("$url\n");
1175 0           push(@hashes, sha256($url));
1176 0           $self->debug("$url " . $self->hex_to_ascii(sha256($url)) . "\n");
1177             }
1178              
1179 0           return @hashes;
1180             }
1181              
1182             =head2 request_full_hash()
1183              
1184             Request full full hashes for specific prefixes from Google.
1185              
1186             =cut
1187              
1188             sub request_full_hash {
1189 0     0 1   my ($self, %args) = @_;
1190 0   0       my $prefixes = $args{prefixes} || return ();
1191 0   0       my $size = $args{size} || length $prefixes->[0];
1192              
1193             # # Handle errors
1194 0           my $i = 0;
1195 0           my $errors;
1196             my $delay = sub {
1197 0     0     my $time = shift;
1198 0 0         if ((time() - $errors->{timestamp}) < $time) {
1199 0           splice(@$prefixes, $i, 1);
1200             }
1201             else {
1202 0           $i++;
1203             }
1204 0           };
1205              
1206 0           while ($i < scalar @$prefixes) {
1207 0           my $prefix = $prefixes->[$i];
1208              
1209 0           $errors = $self->{storage}->get_full_hash_error(prefix => $prefix);
1210 0 0 0       if (defined $errors && $errors->{errors} > 2) { # 2 errors is OK
1211             $errors->{errors} == 3 ? $delay->(30 * 60) # 30 minutes
1212 0 0         : $errors->{errors} == 4 ? $delay->(60 * 60) # 1 hour
    0          
1213             : $delay->(2 * 60 * 60); # 2 hours
1214             }
1215             else {
1216 0           $i++;
1217             }
1218             }
1219              
1220 0           my $url = $self->{server} . "gethash?client=api&key=" . $self->{key} . "&appver=$VERSION&pver=" . $self->{version};
1221              
1222 0           my $prefix_list = join('', @$prefixes);
1223 0           my $header = "$size:" . scalar @$prefixes * $size;
1224              
1225             # print @{$args{prefixes}}, "\n";
1226             # print $$prefixes[0], "\n"; return;
1227              
1228              
1229 0           my $res = $self->ua->post($url, Content => "$header\n$prefix_list");
1230              
1231 0 0         if (! $res->is_success) {
1232 0           $self->error("Full hash request failed\n");
1233 0           $self->debug($res->as_string . "\n");
1234              
1235 0           foreach my $prefix (@$prefixes) {
1236 0           my $errors = $self->{storage}->get_full_hash_error(prefix => $prefix);
1237 0 0 0       if (defined $errors && (
      0        
1238             $errors->{errors} >=2 # backoff mode
1239             || $errors->{errors} == 1 && (time() - $errors->{timestamp}) > 5 * 60)) { # 5 minutes
1240 0           $self->{storage}->full_hash_error(prefix => $prefix, timestamp => time()); # more complicate than this, need to check time between 2 errors
1241             }
1242             }
1243              
1244 0           return ();
1245             }
1246             else {
1247 0           $self->debug("Full hash request OK\n");
1248              
1249 0           foreach my $prefix (@$prefixes) {
1250 0           $self->{storage}->full_hash_ok(prefix => $prefix, timestamp => time());
1251             }
1252             }
1253              
1254 0           $self->debug($res->request->as_string . "\n");
1255 0           $self->debug($res->as_string . "\n");
1256             # $self->debug(substr($res->content, 0, 250), "\n\n");
1257              
1258 0           return $self->parse_full_hashes($res->content);
1259             }
1260              
1261             =head2 parse_full_hashes()
1262              
1263             Process the request for full hashes from Google.
1264              
1265             =cut
1266              
1267             sub parse_full_hashes {
1268 0     0 1   my ($self, $data) = @_;
1269              
1270 0           my @hashes = ();
1271              
1272             # 900
1273             # goog-malware-shavar:32:2:m
1274             # 01234567890123456789012345678901987654321098765432109876543210982
1275             # AA3
1276              
1277             # cache life time
1278 0           my $life = 0;
1279 0 0         if ($data =~ s/^(\d+)\n//) {
1280 0           $life = $1;
1281 0           $self->debug("Full hash life time: ", $life, "\n");
1282             }
1283             else {
1284 0           $self->error("Life time not found\n");
1285             }
1286              
1287 0           while (length $data > 0) {
1288 0 0         if ($data !~ /^[a-z-]+:\d+:\d+(:m)?\n/gi) { # goog-malware-shavar:32:1:m
1289 0           $self->error("list not found\n");
1290 0           return ();
1291             }
1292 0           $data =~ s/^([a-z-]+)://;
1293 0           my $list = $1;
1294            
1295 0           $data =~ s/^(\d+)://;
1296 0           my $length = $1;
1297 0           $self->debug("Full hash length: ", $length, "\n");
1298              
1299 0           $data =~ s/^(\d+)//;
1300 0           my $num = $1;
1301            
1302 0           $self->debug("Number of full hashes returned: ", $num, "\n");
1303              
1304 0           my $metadata = 0;
1305 0 0         if ($data =~ s/:m[\r\n]//) {
1306 0           $metadata = 1;
1307             }
1308              
1309 0           my $current = 0;
1310 0           my @local_hashes = ();
1311 0           while ($current < $num) {
1312 0           my $hash = substr($data, 0, $length, '');
1313 0           push(@local_hashes, { hash => $hash, list => $list, life => $life, type => 0 });
1314              
1315 0           $current ++;
1316             }
1317              
1318 0 0         if ($metadata) {
1319 0           my $count = 0;
1320 0           while ($data =~ s/(\d+)[\r\n]//) {
1321 0           my $meta_length = $1;
1322              
1323 0           my $info = substr($data, 0, $meta_length, '');
1324 0           $self->debug("Metadata: $info\n");
1325 0           my $extra = MalwarePatternType->decode($info);
1326              
1327             # update the type
1328 0           my $hash = $local_hashes[$count];
1329 0           $hash->{type} = $extra->{pattern_type};
1330 0           $local_hashes[$count] = $hash;
1331              
1332 0           $count++;
1333             }
1334             }
1335              
1336              
1337 0           push(@hashes, @local_hashes);
1338             }
1339              
1340 0           $self->debug("Number of hashes: ", scalar(@hashes), "\n");
1341 0           return @hashes;
1342             }
1343              
1344              
1345             =head2 create_range()
1346              
1347             Create a list of ranges (1-3, 5, 7-11) from a list of numbers.
1348              
1349             =cut
1350              
1351             sub create_range {
1352 0     0 1   my ($self, %args) = @_;
1353 0   0       my $numbers = $args{numbers} || []; # should already be ordered
1354              
1355 0 0         return '' if (scalar @$numbers == 0);
1356              
1357 0           my $range = $$numbers[0];
1358 0           my $new_range = 0;
1359 0           for(my $i = 1; $i < scalar @$numbers; $i++) {
1360             # next if ($$numbers[$i] == $$numbers[$i-1]); # should not happen
1361              
1362 0 0         if ($$numbers[$i] != $$numbers[$i-1] + 1) {
    0          
1363 0 0 0       $range .= $$numbers[$i-1] if ($i > 1 && $new_range == 1);
1364 0           $range .= ',' . $$numbers[$i];
1365              
1366 0           $new_range = 0
1367             }
1368             elsif ($new_range == 0) {
1369 0           $range .= "-";
1370 0           $new_range = 1;
1371             }
1372             }
1373 0 0         $range .= $$numbers[scalar @$numbers - 1] if ($new_range == 1);
1374              
1375 0           return $range;
1376             }
1377              
1378             =head2 expand_range()
1379              
1380             Explode list of ranges (1-3, 5, 7-11) into a list of numbers (1,2,3,5,7,8,9,10,11).
1381              
1382             =cut
1383              
1384             sub expand_range {
1385 0     0 1   my ($self, %args) = @_;
1386 0   0       my $range = $args{range} || return ();
1387              
1388 0           my @list = ();
1389 0           my @elements = split /,/, $range;
1390              
1391 0           foreach my $data (@elements) {
1392 0 0         if ($data =~ /^\d+$/) { # single number
    0          
1393 0           push(@list, $data);
1394             }
1395             elsif ($data =~ /^(\d+)-(\d+)$/) {
1396 0           my $start = $1;
1397 0           my $end = $2;
1398              
1399 0           for(my $i = $start; $i <= $end; $i++) {
1400 0           push(@list, $i);
1401             }
1402             }
1403             }
1404              
1405 0           return @list;
1406             }
1407              
1408             =head1 CHANGELOG
1409              
1410             =over 4
1411              
1412             =item 0.7
1413              
1414             Remove \r from metata data
1415              
1416             =item 0.6
1417              
1418             Many fixes: local database update, lcoal database lookup, full hash response parsing, etc.
1419              
1420             =back
1421              
1422             =head1 SEE ALSO
1423              
1424             See L for handling Google Safe Browsing v3.
1425              
1426             See L for the list of public functions.
1427              
1428             See L for a back-end using Sqlite.
1429              
1430             Google Safe Browsing v3 API: L
1431              
1432              
1433             =head1 AUTHOR
1434              
1435             Julien Sobrier, Ejulien@sobrier.netE
1436              
1437             =head1 COPYRIGHT AND LICENSE
1438              
1439             Copyright (C) 2015 by Julien Sobrier
1440              
1441             This library is free software; you can redistribute it and/or modify
1442             it under the same terms as Perl itself, either Perl version 5.8.8 or,
1443             at your option, any later version of Perl 5 you may have available.
1444              
1445              
1446             =cut
1447              
1448             1;
1449             __END__